diff --git a/.circleci/config.yml b/.circleci/config.yml index adfcd2f10aa2..dd53ff928afb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -93,7 +93,7 @@ jobs: - brew-cache-v1 - checkout - run: bin/ci prepare_system - - run: echo 'export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig"' >> $BASH_ENV + - run: echo 'export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/openssl@1.1/lib/pkgconfig"' >> $BASH_ENV - run: echo 'export CURRENT_TAG="$CIRCLE_TAG"' >> $BASH_ENV - run: bin/ci prepare_build - run: bin/ci build @@ -172,7 +172,7 @@ jobs: - run: | git clone https://github.com/crystal-lang/distribution-scripts.git ~/distribution-scripts cd ~/distribution-scripts - git checkout cfe843ff938697174afea976f4499fef806b5286 + git checkout e1704da2a215264ea8e101eb15d4ec1baae98222 # persist relevant information for build process - run: | cd ~/distribution-scripts @@ -183,7 +183,7 @@ jobs: echo "export CRYSTAL_SHA1=$CIRCLE_SHA1" >> build.env # Which previous version use - export PREVIOUS_CRYSTAL_BASE_URL="https://github.com/crystal-lang/crystal/releases/download/0.34.0/crystal-0.34.0-1" + export PREVIOUS_CRYSTAL_BASE_URL="https://github.com/crystal-lang/crystal/releases/download/0.35.1/crystal-0.35.1-1" echo "export PREVIOUS_CRYSTAL_RELEASE_LINUX64_TARGZ=${PREVIOUS_CRYSTAL_BASE_URL}-linux-x86_64.tar.gz" >> build.env echo "export PREVIOUS_CRYSTAL_RELEASE_LINUX32_TARGZ=${PREVIOUS_CRYSTAL_BASE_URL}-linux-i686.tar.gz" >> build.env echo "export PREVIOUS_CRYSTAL_RELEASE_DARWIN_TARGZ=${PREVIOUS_CRYSTAL_BASE_URL}-darwin-x86_64.tar.gz" >> build.env @@ -225,12 +225,17 @@ jobs: steps: - attach_workspace: at: /tmp/workspace + - checkout + - run: | + # We need CRYSTAL_VERSION in prepare_nightly to use src/VERSION so we publish them as x.y.z-dev in apt/rpm + # + # How to brand it + echo "export CRYSTAL_VERSION=$(cat src/VERSION)" >> /tmp/workspace/distribution-scripts/build.env + # + # TODO: We might want to do that on docker images also to support updates on multiple development versions the same date. - run: | cd /tmp/workspace/distribution-scripts - # How to brand it - export VERSION=nightly-$(date '+%Y%m%d') - echo "export CRYSTAL_VERSION=$VERSION" >> build.env echo "export DOCKER_TAG=nightly" >> build.env # Build from working directory (needed for omnibus and when version does not match branch/tag) @@ -358,6 +363,24 @@ jobs: paths: - build + dist_bintray_nightly: + machine: true + steps: + - attach_workspace: + at: /tmp/workspace + - run: + no_output_timeout: 20m + command: | + cd /tmp/workspace/distribution-scripts + source build.env + cd bintray + ./publish-nightly.sh $CRYSTAL_VERSION $(date '+%Y-%m-%d') \ + /tmp/workspace/build/crystal-*-linux-x86_64.tar.gz \ + /tmp/workspace/build/crystal-*-linux-i686.tar.gz + - store_artifacts: + path: /tmp/workspace/distribution-scripts/bintray/build/unsigned + destination: unsigned + dist_docker: machine: true steps: @@ -434,6 +457,7 @@ jobs: - attach_workspace: at: /tmp/workspace - run: + # $SNAPCRAFT_TOKEN is generated using `snapcraft export-login --snaps crystal --channels edge,edge/* -` command: | cd /tmp/workspace/distribution-scripts source build.env @@ -521,13 +545,14 @@ workflows: branches: ignore: - /release\/.+/ + - /package\/.+/ - /.*\bci\b.*/ - test_linux32_std: filters: *unless_maintenance - test_alpine: filters: *unless_maintenance - - test_darwin: - filters: *unless_maintenance + # - test_darwin: + # filters: *unless_maintenance - test_preview_mt: filters: *unless_maintenance - check_format: @@ -552,8 +577,8 @@ workflows: filters: *per_tag - test_alpine: filters: *per_tag - - test_darwin: - filters: *per_tag + # - test_darwin: + # filters: *per_tag - test_preview_mt: filters: *per_tag - check_format: @@ -627,7 +652,7 @@ workflows: - test_linux - test_linux32_std - test_alpine - - test_darwin + # - test_darwin - test_preview_mt - check_format - prepare_common @@ -643,6 +668,10 @@ workflows: - dist_darwin: requires: - prepare_nightly + - dist_bintray_nightly: + requires: + - dist_linux + - dist_linux32 - dist_docker: requires: - dist_linux @@ -687,8 +716,8 @@ workflows: filters: *maintenance - test_alpine: filters: *maintenance - - test_darwin: - filters: *maintenance + # - test_darwin: + # filters: *maintenance - test_preview_mt: filters: *maintenance - check_format: @@ -751,3 +780,33 @@ workflows: - dist_darwin - dist_snap - dist_docs + + package_build: + jobs: + - prepare_common: + filters: &package + branches: + only: + - /package\/.+/ + - prepare_maintenance: + filters: *package + requires: + - prepare_common + - dist_linux: + filters: *package + requires: + - prepare_maintenance + - dist_linux32: + filters: *package + requires: + - prepare_maintenance + - dist_darwin: + filters: *package + requires: + - prepare_maintenance + - dist_artifacts: + filters: *package + requires: + - dist_linux + - dist_linux32 + - dist_darwin diff --git a/.github/workflows/aarch64.yml b/.github/workflows/aarch64.yml new file mode 100644 index 000000000000..491afbcbd10a --- /dev/null +++ b/.github/workflows/aarch64.yml @@ -0,0 +1,107 @@ +name: AArch64 CI + +on: [push, pull_request] + +jobs: + musl-build: + runs-on: [linux, ARM64] + if: github.repository == 'crystal-lang/crystal' + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + - name: Build Crystal + uses: docker://jhass/crystal:0.35.0-alpine-build + with: + args: make crystal + - name: Upload Crystal executable + uses: actions/upload-artifact@v1 + with: + name: crystal-aarch64-musl + path: .build/crystal + musl-test-stdlib: + needs: musl-build + runs-on: [linux, ARM64] + if: github.repository == 'crystal-lang/crystal' + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + - name: Download Crystal executable + uses: actions/download-artifact@v1 + with: + name: crystal-aarch64-musl + path: .build/ + - name: Mark downloaded compiler as executable + run: chmod +x .build/crystal + - name: Run stdlib specs + uses: docker://jhass/crystal:0.35.0-alpine-build + with: + args: make std_spec + musl-test-compiler: + needs: musl-build + runs-on: [linux, ARM64] + if: github.repository == 'crystal-lang/crystal' + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + - name: Download Crystal executable + uses: actions/download-artifact@v1 + with: + name: crystal-aarch64-musl + path: .build/ + - name: Mark downloaded compiler as executable + run: chmod +x .build/crystal + - name: Run compiler specs + uses: docker://jhass/crystal:0.35.0-alpine-build + with: + args: make compiler_spec + gnu-build: + runs-on: [linux, ARM64] + if: github.repository == 'crystal-lang/crystal' + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + - name: Build Crystal + uses: docker://jhass/crystal:0.35.0-build + with: + args: make crystal + - name: Upload Crystal executable + uses: actions/upload-artifact@v1 + with: + name: crystal-aarch64-gnu + path: .build/crystal + gnu-test-stdlib: + needs: gnu-build + runs-on: [linux, ARM64] + if: github.repository == 'crystal-lang/crystal' + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + - name: Download Crystal executable + uses: actions/download-artifact@v1 + with: + name: crystal-aarch64-gnu + path: .build/ + - name: Mark downloaded compiler as executable + run: chmod +x .build/crystal + - name: Run stdlib specs + uses: docker://jhass/crystal:0.35.0-build + with: + args: make std_spec + gnu-test-compiler: + needs: gnu-build + runs-on: [linux, ARM64] + if: github.repository == 'crystal-lang/crystal' + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + - name: Download Crystal executable + uses: actions/download-artifact@v1 + with: + name: crystal-aarch64-gnu + path: .build/ + - name: Mark downloaded compiler as executable + run: chmod +x .build/crystal + - name: Run compiler specs + uses: docker://jhass/crystal:0.35.0-build + with: + args: make compiler_spec diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 000000000000..2d36da97594c --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,101 @@ +name: Linux CI + +on: [push, pull_request] + +env: + TRAVIS_OS_NAME: linux + SPEC_SPLIT_DOTS: 160 + +jobs: + test_linux: + env: + ARCH: x86_64 + ARCH_CMD: linux64 + runs-on: ubuntu-latest + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + + - name: Prepare System + run: bin/ci prepare_system + + - name: Prepare Build + run: bin/ci prepare_build + + - name: Test + run: bin/ci build + + test_linux_32: + env: + ARCH: i386 + ARCH_CMD: linux32 + runs-on: ubuntu-latest + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + + - name: Prepare System + run: bin/ci prepare_system + + - name: Prepare Build + run: bin/ci prepare_build + + - name: Test + run: bin/ci with_build_env 'make std_spec threads=1' + + test_alpine: + env: + ARCH: x86_64-musl + ARCH_CMD: linux64 + runs-on: ubuntu-latest + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + + - name: Prepare System + run: bin/ci prepare_system + + - name: Prepare Build + run: bin/ci prepare_build + + - name: Test + run: bin/ci build + + test_preview_mt: + env: + ARCH: x86_64 + ARCH_CMD: linux64 + runs-on: ubuntu-latest + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + + - name: Prepare System + run: bin/ci prepare_system + + - name: Prepare Build + run: bin/ci prepare_build + + - name: Make Crystal + run: bin/ci with_build_env 'make crystal' + + - name: Test + run: bin/ci with_build_env 'CRYSTAL_WORKERS=4 make std_spec threads=1 FLAGS="-D preview_mt"' + + check_format: + env: + ARCH: x86_64 + ARCH_CMD: linux64 + runs-on: ubuntu-latest + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + + - name: Prepare System + run: bin/ci prepare_system + + - name: Prepare Build + run: bin/ci prepare_build + + - name: Check Format + run: bin/ci format diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 000000000000..0f10ac993bc4 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,29 @@ +name: macOS CI + +on: [push, pull_request] + +env: + SPEC_SPLIT_DOTS: 160 + CI_NIX_SHELL: true + +jobs: + test_macos: + runs-on: macos-latest + steps: + - name: Download Crystal source + uses: actions/checkout@v2 + + - uses: cachix/install-nix-action@v10 + - uses: cachix/cachix-action@v6 + with: + name: crystal-ci + signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' + + - name: Prepare System + run: bin/ci prepare_system + + - name: Prepare Build + run: bin/ci prepare_build + + - name: Test + run: bin/ci build diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index cd7d17412754..3edaa3f89364 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -5,7 +5,7 @@ on: [push, pull_request] jobs: linux-job: runs-on: ubuntu-latest - container: crystallang/crystal:0.34.0-build + container: crystallang/crystal:0.35.1-build steps: - name: Download Crystal source uses: actions/checkout@v2 @@ -21,7 +21,7 @@ jobs: uses: actions/upload-artifact@v1 with: name: objs - path: crystal.o + path: crystal.obj windows-job: needs: linux-job @@ -60,7 +60,7 @@ jobs: if: steps.cache-libs.outputs.cache-hit != 'true' working-directory: ./bdwgc run: | - cmake . -DBUILD_SHARED_LIBS=OFF -Denable_large_config=ON -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded + cmake . -DBUILD_SHARED_LIBS=OFF -Denable_large_config=ON -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF cmake --build . --config Release - name: Download libpcre if: steps.cache-libs.outputs.cache-hit != 'true' @@ -72,7 +72,7 @@ jobs: if: steps.cache-libs.outputs.cache-hit != 'true' working-directory: ./pcre run: | - cmake . -DBUILD_SHARED_LIBS=OFF -DPCRE_SUPPORT_UNICODE_PROPERTIES=ON -DPCRE_SUPPORT_JIT=ON -DPCRE_STATIC_RUNTIME=ON + cmake . -DBUILD_SHARED_LIBS=OFF -DPCRE_SUPPORT_UNICODE_PROPERTIES=ON -DPCRE_SUPPORT_JIT=ON -DPCRE_STATIC_RUNTIME=ON -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF cmake --build . --config Release - name: Download zlib if: steps.cache-libs.outputs.cache-hit != 'true' @@ -84,7 +84,7 @@ jobs: if: steps.cache-libs.outputs.cache-hit != 'true' working-directory: ./zlib run: | - cmake . -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded + cmake . -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF cmake --build . --config Release - name: Download libyaml if: steps.cache-libs.outputs.cache-hit != 'true' @@ -96,7 +96,20 @@ jobs: if: steps.cache-libs.outputs.cache-hit != 'true' working-directory: ./libyaml run: | - cmake . -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded + cmake . -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF + cmake --build . --config Release + - name: Download libxml2 + if: steps.cache-libs.outputs.cache-hit != 'true' + uses: actions/checkout@v2 + with: + repository: GNOME/libxml2 + ref: a230b728f1289dd24c1666856ac4fb55579c6dfb # master @ 2020-05-04 + path: libxml2 + - name: Build libxml2 + if: steps.cache-libs.outputs.cache-hit != 'true' + working-directory: ./libxml2 + run: | + cmake . -DBUILD_SHARED_LIBS=OFF -DLIBXML2_WITH_HTTP=OFF -DLIBXML2_WITH_FTP=OFF -DLIBXML2_WITH_TESTS=OFF -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF cmake --build . --config Release - name: Gather libraries if: steps.cache-libs.outputs.cache-hit != 'true' @@ -106,6 +119,7 @@ jobs: mv bdwgc/Release/gc.lib libs/ mv zlib/Release/zlibstatic.lib libs/z.lib mv libyaml/Release/yaml.lib libs/ + mv libxml2/Release/libxml2s.lib libs/xml2.lib - name: Cache LLVM id: cache-llvm @@ -124,7 +138,7 @@ jobs: if: steps.cache-llvm.outputs.cache-hit != 'true' working-directory: ./llvm-src run: | - cmake . -Thost=x64 -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_USE_CRT_RELEASE=MT -DBUILD_SHARED_LIBS=OFF + cmake . -Thost=x64 -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_USE_CRT_RELEASE=MT -DBUILD_SHARED_LIBS=OFF -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF cmake --build . --config Release - name: Gather LLVM if: steps.cache-llvm.outputs.cache-hit != 'true' @@ -134,7 +148,7 @@ jobs: - name: Set up environment run: | - echo "::set-env name=CRYSTAL_PATH::src" + echo "::set-env name=CRYSTAL_PATH::$(pwd)\src" echo "::set-env name=LIB::${env:LIB};$(pwd)\libs" echo "::set-env name=TERM::dumb" echo "::set-env name=LLVM_CONFIG::$(pwd)\llvm\bin\llvm-config.exe" @@ -146,21 +160,22 @@ jobs: name: objs - name: Build LLVM extensions run: | - cl /MT /c src\llvm\ext\llvm_ext.cc -I llvm\include /Fosrc\llvm\ext\llvm_ext.o + cl /MT /c src\llvm\ext\llvm_ext.cc -I llvm\include /Fosrc\llvm\ext\llvm_ext.obj - name: Link Crystal executable run: | - Invoke-Expression "cl objs\crystal.o /Fecrystal-cross src\llvm\ext\llvm_ext.o $(llvm\bin\llvm-config.exe --libs) libs\pcre.lib libs\gc.lib advapi32.lib libcmt.lib legacy_stdio_definitions.lib /F10000000" + Invoke-Expression "cl objs\crystal.obj /Fecrystal-cross src\llvm\ext\llvm_ext.obj $(llvm\bin\llvm-config.exe --libs) libs\pcre.lib libs\gc.lib advapi32.lib libcmt.lib legacy_stdio_definitions.lib /F10000000" - name: Re-build Crystal run: | .\crystal-cross.exe build src/compiler/crystal.cr -Di_know_what_im_doing -Dwithout_playground --link-flags=/F10000000 + mv crystal.exe bin/ - - name: Gather Crystal executable + - name: Gather Crystal binaries run: | mkdir crystal - cp crystal.exe crystal/ + cp bin/crystal.exe crystal/ cp libs/* crystal/ - - name: Upload Crystal executable + - name: Upload Crystal binaries uses: actions/upload-artifact@v1 with: name: crystal @@ -168,7 +183,14 @@ jobs: - name: Build stdlib specs executable run: | - .\crystal.exe build spec/std_spec.cr --exclude-warnings spec/std -Dwithout_openssl -Di_know_what_im_doing + bin\crystal.exe build spec/std_spec.cr --exclude-warnings spec/std --exclude-warnings spec/compiler -Dwithout_openssl -Di_know_what_im_doing - name: Run stdlib specs run: | .\std_spec.exe + + - name: Build compiler specs executable + run: | + bin\crystal.exe build spec/compiler_spec.cr --exclude-warnings spec/std --exclude-warnings spec/compiler -Dwithout_playground -Di_know_what_im_doing + - name: Run compiler specs + run: | + .\compiler_spec.exe diff --git a/.gitignore b/.gitignore index 2ce57d81ab1d..ed709c9b2cda 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ all_spec /tmp /docs/ /src/llvm/ext/llvm_ext.o +/src/llvm/ext/llvm_ext.obj /src/llvm/ext/llvm_ext.dwo /src/ext/*.o /src/ext/libcrystal.a diff --git a/.travis.yml b/.travis.yml index fffcb0697b41..c368c4123e39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,12 +41,5 @@ env: - secure: Zd/tZVmV2dRMao9z+ky5BywSKuWOF3MiKsZetwd1upZ+uj9qzfbOZMnWFW9dlA+Co4MyYqP/I6ADzRpoKLINUqEIPcAPNYQB1qG79SafrRAvTqcjtEHTn2wXh2ZGu3f1T+SCK0ZD3xx1ML8502ENzXjvq+dEmi4kknqmPudkb6k= notifications: - irc: - channels: - - secure: iarUM4VAMZxdZZQxDrQy+MpxygaFFXJJ1bxvafehnk2vjWbifmnQHZx5z3POaSfp76q+hdqSgBrK/QnGwd8NY70wD+bR0Vb5JQoHwEyHyGbLczHp606SXRl7c/ZMozfOk4bo0X5EPPtie+2mtkjSUB03TI4NYY/5LMFhCw79zsU= - use_notice: true - skip_join: true - template: - - "%{repository_slug}#%{commit} (%{branch} - %{commit_subject}): %{message} %{build_url}" slack: secure: Ng3nTqGWY+9p1pS6yjGqDhmRvdgbIZgTNpMWbO/ngwpCyicmD3jafZkShqqXbULZTJJr3OxIGzi6GHGusT0Ic/Pi9JCM3X3v/xuBruKIR+EnNyPo7IL4ZYAlwnXyJHlCHHDBq0gSHGvGJwsXn6IgZBPRfeIq+CCyQHVPyvc9EHE= diff --git a/Brewfile b/Brewfile new file mode 100644 index 000000000000..22ffcf245edb --- /dev/null +++ b/Brewfile @@ -0,0 +1,6 @@ +brew "gmp" +brew "libevent" +brew "pcre" +brew "pkg-config" +brew "openssl@1.1" +brew "llvm@10", link: true, conflicts_with: ["python@2"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 12023d944a5e..da02a193c68c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,282 @@ +# 0.35.1 (2020-06-19) + +## Standard library + +### Collections + +- Remove `Hash#each` type restriction to allow working with splats. ([#9456](https://github.com/crystal-lang/crystal/pull/9456), thanks @bcardiff) + +### Networking + +- Revert `IO#write` changes in 0.35.0 and let it return Nil. ([#9469](https://github.com/crystal-lang/crystal/pull/9469), thanks @bcardiff) +- Avoid leaking logging context in HTTP request handlers. ([#9494](https://github.com/crystal-lang/crystal/pull/9494), thanks @Blacksmoke16) + +### Crypto + +- Use less strict cipher compatibility for OpenSSL client context. ([#9459](https://github.com/crystal-lang/crystal/pull/9459), thanks @straight-shoota) +- Fix `Digest::Base` block argument type restrictions. ([#9500](https://github.com/crystal-lang/crystal/pull/9500), thanks @straight-shoota) + +### Logging + +- Fix `Log.context.set` docs for hash based data. ([#9470](https://github.com/crystal-lang/crystal/pull/9470), thanks @bcardiff) + +## Compiler + +- Show warnings even if there are errors. ([#9461](https://github.com/crystal-lang/crystal/pull/9461), thanks @asterite) +- Fix parsing of `{foo: X, typeof: Y}` type. ([#9453](https://github.com/crystal-lang/crystal/pull/9453), thanks @MakeNowJust) +- Fix parsing of proc in hash `of` key type. ([#9458](https://github.com/crystal-lang/crystal/pull/9458), thanks @MakeNowJust) +- Revert debug level information changes in specs to fix 32 bits builds. ([#9466](https://github.com/crystal-lang/crystal/pull/9466), thanks @bcardiff) + +## Others + +- CI improvements and housekeeping. ([#9455](https://github.com/crystal-lang/crystal/pull/9455), thanks @bcardiff) +- Code formatting. ([#9482](https://github.com/crystal-lang/crystal/pull/9482), thanks @MakeNowJust) + +# 0.35.0 (2020-06-09) + +## Language changes + +- **(breaking-change)** Let `case when` be non-exhaustive, introduce `case in` as exhaustive. ([#9258](https://github.com/crystal-lang/crystal/pull/9258), [#9045](https://github.com/crystal-lang/crystal/pull/9045), thanks @asterite) +- Allow `->@ivar.foo` and `->@@cvar.foo` expressions. ([#9268](https://github.com/crystal-lang/crystal/pull/9268), thanks @MakeNowJust) + +### Macros + +- Allow executing OpAssign (`+=`, `||=`, etc.) inside macros. ([#9409](https://github.com/crystal-lang/crystal/pull/9409), thanks @asterite) + +## Standard library + +- **(breaking-change)** Refactor to standardize on first argument for methods receiving `IO`. ([#9134](https://github.com/crystal-lang/crystal/pull/9134), [#9289](https://github.com/crystal-lang/crystal/pull/9289), [#9303](https://github.com/crystal-lang/crystal/pull/9303), [#9318](https://github.com/crystal-lang/crystal/pull/9318), thanks @straight-shoota, @bcardiff, @oprypin) +- **(breaking-change)** Cleanup Digest and OpenSSL::Digest. ([#8426](https://github.com/crystal-lang/crystal/pull/8426), thanks @didactic-drunk) +- Fix `Enum#to_s` for private enum. ([#9126](https://github.com/crystal-lang/crystal/pull/9126), thanks @straight-shoota) +- Refactor `Benchmark::IPS::Entry` to use `UInt64` in `bytes_per_op`. ([#9081](https://github.com/crystal-lang/crystal/pull/9081), thanks @jhass) +- Add `Experimental` annotation and doc label. ([#9244](https://github.com/crystal-lang/crystal/pull/9244), thanks @bcardiff) +- Add subcommands to `OptionParser`. ([#9009](https://github.com/crystal-lang/crystal/pull/9009), [#9133](https://github.com/crystal-lang/crystal/pull/9133), thanks @RX14, @Sija) +- Make `NamedTuple#sorted_keys` public. ([#9263](https://github.com/crystal-lang/crystal/pull/9263), thanks @waj) +- Fix example codes in multiple places. ([#9203](https://github.com/crystal-lang/crystal/pull/9203), thanks @maiha) + +### Macros + +- **(breaking-change)** Remove top-level `assert_responds_to` macro. ([#9085](https://github.com/crystal-lang/crystal/pull/9085), thanks @bcardiff) +- **(breaking-change)** Drop top-level `parallel` macro. ([#9097](https://github.com/crystal-lang/crystal/pull/9097), thanks @bcardiff) +- Fix lazy property not forwarding annotations. ([#9140](https://github.com/crystal-lang/crystal/pull/9140), thanks @asterite) +- Add `host_flag?` macro method, not affected by cross-compilation. ([#9049](https://github.com/crystal-lang/crystal/pull/9049), thanks @oprypin) +- Add `.each` and `.each_with_index` to various macro types. ([#9120](https://github.com/crystal-lang/crystal/pull/9120), thanks @Blacksmoke16) +- Add `StringLiteral#titleize` macro method. ([#9269](https://github.com/crystal-lang/crystal/pull/9269), thanks @MakeNowJust) +- Add `TypeNode` methods to check what "type" the node is. ([#9270](https://github.com/crystal-lang/crystal/pull/9270), thanks @Blacksmoke16) +- Fix support `TypeNode.name(generic_args: false)` for generic instances. ([#9224](https://github.com/crystal-lang/crystal/pull/9224), thanks @Blacksmoke16) + +### Numeric + +- **(breaking-change)** Add `Int#digits`, reverse `BigInt#digits` result. ([#9383](https://github.com/crystal-lang/crystal/pull/9383), thanks @asterite) +- Fix overflow checking for operations with mixed sign. ([#9403](https://github.com/crystal-lang/crystal/pull/9403), thanks @waj) +- Add `BigInt#factorial` using GMP. ([#9132](https://github.com/crystal-lang/crystal/pull/9132), thanks @peheje) + +### Text + +- Add `String#titleize`. ([#9204](https://github.com/crystal-lang/crystal/pull/9204), thanks @hugopl) +- Add `Regex#matches?` and `String#matches?`. ([#8989](https://github.com/crystal-lang/crystal/pull/8989), thanks @MakeNowJust) +- Add `IO` overloads to various `String` case methods. ([#9236](https://github.com/crystal-lang/crystal/pull/9236), thanks @Blacksmoke16) +- Improve docs examples regarding `Regex::MatchData`. ([#9010](https://github.com/crystal-lang/crystal/pull/9010), thanks @MakeNowJust) +- Improve docs on `String` methods. ([#8447](https://github.com/crystal-lang/crystal/pull/8447), thanks @jan-zajic) + +### Collections + +- **(breaking-change)** Add `Enumerable#first` with fallback block. ([#8999](https://github.com/crystal-lang/crystal/pull/8999), thanks @MakeNowJust) +- Fix `Array#delete_at` bug with negative start index. ([#9399](https://github.com/crystal-lang/crystal/pull/9399), thanks @asterite) +- Fix `Enumerable#{zip,zip?}` when self is an `Iterator`. ([#9330](https://github.com/crystal-lang/crystal/pull/9330), thanks @mneumann) +- Make `Range#each` and `Range#reverse_each` work better with end/begin-less values. ([#9325](https://github.com/crystal-lang/crystal/pull/9325), thanks @asterite) +- Improve docs on `Hash`. ([#8887](https://github.com/crystal-lang/crystal/pull/8887), thanks @rdp) + +### Serialization + +- **(breaking-change)** Deprecate `JSON.mapping` and `YAML.mapping`. ([#9272](https://github.com/crystal-lang/crystal/pull/9272), thanks @straight-shoota) +- **(breaking-change)** Make `INI` a module. ([#9408](https://github.com/crystal-lang/crystal/pull/9408), thanks @j8r) +- Fix integration between `record` macro and `JSON::Serializable`/`YAML::Serializable` regarding default values. ([#9063](https://github.com/crystal-lang/crystal/pull/9063), thanks @Blacksmoke16) +- Fix `XML.parse` invalid mem access in multi-thread. ([#9098](https://github.com/crystal-lang/crystal/pull/9098), thanks @bcardiff, @asterite) +- Fix double string escape in `XML::Node#content=`. ([#9300](https://github.com/crystal-lang/crystal/pull/9300), thanks @straight-shoota) +- Improve xpath regarding namespaces. ([#9288](https://github.com/crystal-lang/crystal/pull/9288), thanks @asterite) +- Escape CDATA end sequences. ([#9230](https://github.com/crystal-lang/crystal/pull/9230), thanks @Blacksmoke16) +- Add `JSON` and `YAML` serialization to `Path`. ([#9156](https://github.com/crystal-lang/crystal/pull/9156), thanks @straight-shoota) +- Specify pkg-config name for `libyaml`. ([#9426](https://github.com/crystal-lang/crystal/pull/9426), thanks @jhass) +- Specify pkg-config name for `libxml2`. ([#9436](https://github.com/crystal-lang/crystal/pull/9436), thanks @Blacksmoke16) +- Make YAML specs robust against libyaml 0.2.5. ([#9427](https://github.com/crystal-lang/crystal/pull/9427), thanks @jhass) + +### Time + +- **(breaking-change)** Support different number of fraction digits for RFC3339 time format. ([#9283](https://github.com/crystal-lang/crystal/pull/9283), thanks @waj) +- Fix parsing AM/PM hours. ([#9334](https://github.com/crystal-lang/crystal/pull/9334), thanks @straight-shoota) +- Improve `File.utime` precision from second to 100-nanosecond on Windows. ([#9344](https://github.com/crystal-lang/crystal/pull/9344), thanks @kubo) + +### Files + +- **(breaking-change)** Move `Flate`, `Gzip`, `Zip`, `Zlib` to `Compress`. ([#8886](https://github.com/crystal-lang/crystal/pull/8886), thanks @bcardiff) +- **(breaking-change)** Cleanup `File` & `FileUtils`. ([#9175](https://github.com/crystal-lang/crystal/pull/9175), thanks @bcardiff) +- Fix realpath on macOS 10.15 (Catalina). ([#9296](https://github.com/crystal-lang/crystal/pull/9296), thanks @waj) +- Fix `File#pos`, `File#seek` and `File#truncate` over 2G on Windows. ([#9015](https://github.com/crystal-lang/crystal/pull/9015), thanks @kubo) +- Fix `File.rename` to overwrite the destination file on Windows, like elsewhere. ([#9038](https://github.com/crystal-lang/crystal/pull/9038), thanks @oprypin) +- Fix `File`'s specs and related exception types on Windows. ([#9037](https://github.com/crystal-lang/crystal/pull/9037), thanks @oprypin) +- Add support for `Path` arguments to multiple methods. ([#9153](https://github.com/crystal-lang/crystal/pull/9153), thanks @straight-shoota) +- Add `Path#each_part` iterator. ([#9138](https://github.com/crystal-lang/crystal/pull/9138), thanks @straight-shoota) +- Add `Path#relative_to`. ([#9169](https://github.com/crystal-lang/crystal/pull/9169), thanks @straight-shoota) +- Add support for `Path` pattern to `Dir.glob`. ([#9420](https://github.com/crystal-lang/crystal/pull/9420), thanks @straight-shoota) +- Implement `File#fsync` on Windows. ([#9257](https://github.com/crystal-lang/crystal/pull/9257), thanks @kubo) +- Refactor `Path` regarding empty and `.`. ([#9137](https://github.com/crystal-lang/crystal/pull/9137), thanks @straight-shoota) + +### Networking + +- **(breaking-change)** Make `IO#skip`, `IO#write` returns the number of bytes it skipped/written as `Int64`. ([#9233](https://github.com/crystal-lang/crystal/pull/9233), [#9363](https://github.com/crystal-lang/crystal/pull/9363), thanks @bcardiff) +- **(breaking-change)** Improve error handling and logging in `HTTP::Server`. ([#9115](https://github.com/crystal-lang/crystal/pull/9115), [#9034](https://github.com/crystal-lang/crystal/pull/9034), thanks @waj, @straight-shoota) +- **(breaking-change)** Change `HTTP::Request#remote_address` type to `Socket::Address?`. ([#9210](https://github.com/crystal-lang/crystal/pull/9210), thanks @waj) +- Fix `flush` methods to always flush underlying `IO`. ([#9320](https://github.com/crystal-lang/crystal/pull/9320), thanks @straight-shoota) +- Fix `HTTP::Server` sporadic failure in SSL handshake. ([#9177](https://github.com/crystal-lang/crystal/pull/9177), thanks @waj) +- `WebSocket` shouldn't reply with same close code. ([#9313](https://github.com/crystal-lang/crystal/pull/9313), thanks @waj) +- Ignore response body during `WebSocket` handshake. ([#9418](https://github.com/crystal-lang/crystal/pull/9418), thanks @waj) +- Treat cookies which expire in this instant as expired. ([#9061](https://github.com/crystal-lang/crystal/pull/9061), thanks @RX14) +- Set `sync` or `flush_on_newline` for standard I/O on Windows. ([#9207](https://github.com/crystal-lang/crystal/pull/9207), thanks @kubo) +- Prefer HTTP basic authentication in OAuth2 client. ([#9127](https://github.com/crystal-lang/crystal/pull/9127), thanks @crush-157) +- Defer request upgrade in `HTTP::Server` (aka: WebSockets). ([#9243](https://github.com/crystal-lang/crystal/pull/9243), thanks @waj) +- Improve `URI::Punycode`, `HTTP::WebSocketHandler`, `HTTP::Status` documentation. ([#9068](https://github.com/crystal-lang/crystal/pull/9068), [#9130](https://github.com/crystal-lang/crystal/pull/9130), [#9180](https://github.com/crystal-lang/crystal/pull/9180), thanks @Blacksmoke16, @dscottboggs, @wontruefree) +- Remove `HTTP::Params::Builder#to_s`, use underlying `IO` directly. ([#9319](https://github.com/crystal-lang/crystal/pull/9319), thanks @straight-shoota) +- Fixed some regular failing specs in multi-thread mode. ([#9412](https://github.com/crystal-lang/crystal/pull/9412), thanks @bcardiff) + +### Crypto + +- **(security)** Update SSL server secure defaults. ([#9026](https://github.com/crystal-lang/crystal/pull/9026), thanks @straight-shoota) +- Add LibSSL `NO_TLS_V1_3` option. ([#9350](https://github.com/crystal-lang/crystal/pull/9350), thanks @lun-4) + +### Logging + +- **(breaking-change)** Rename `Log::Severity::Warning` to `Warn`. Drop `Verbose`. Add `Trace` and `Notice`. ([#9293](https://github.com/crystal-lang/crystal/pull/9293), [#9107](https://github.com/crystal-lang/crystal/pull/9107), [#9316](https://github.com/crystal-lang/crystal/pull/9316), thanks @bcardiff, @paulcsmith) +- **(breaking-change)** Allow local data on entries via `Log::Metadata` and redesign `Log::Context`. ([#9118](https://github.com/crystal-lang/crystal/pull/9118), [#9227](https://github.com/crystal-lang/crystal/pull/9227), [#9150](https://github.com/crystal-lang/crystal/pull/9150), [#9157](https://github.com/crystal-lang/crystal/pull/9157), thanks @bcardiff, @waj) +- **(breaking-change)** Split top-level `Log::Metadata` from `Log::Metadata::Value`, drop immutability via clone, improve performance. ([#9295](https://github.com/crystal-lang/crystal/pull/9295), thanks @bcardiff) +- **(breaking-change)** Rework `Log.setup_from_env` and defaults. ([#9145](https://github.com/crystal-lang/crystal/pull/9145), [#9240](https://github.com/crystal-lang/crystal/pull/9240), thanks @bcardiff) +- Add `Log.capture` spec helper. ([#9201](https://github.com/crystal-lang/crystal/pull/9201), thanks @bcardiff) +- Redesign `Log::Formatter`. ([#9211](https://github.com/crystal-lang/crystal/pull/9211), thanks @waj) +- Add `to_json` for `Log::Context`. ([#9101](https://github.com/crystal-lang/crystal/pull/9101), thanks @paulcsmith) +- Add `Log::IOBackend#new` with `formatter` named argument. ([#9105](https://github.com/crystal-lang/crystal/pull/9105), [#9434](https://github.com/crystal-lang/crystal/pull/9434), thanks @paulcsmith, @bcardiff) +- Allow `nil` as context raw values. ([#9121](https://github.com/crystal-lang/crystal/pull/9121), thanks @bcardiff) +- Add missing `Log#with_context`. ([#9058](https://github.com/crystal-lang/crystal/pull/9058), thanks @bcardiff) +- Fix types referred in documentation. ([#9117](https://github.com/crystal-lang/crystal/pull/9117), thanks @bcardiff) +- Allow override context within logging calls. ([#9146](https://github.com/crystal-lang/crystal/pull/9146), thanks @bcardiff) +- Check severity before backend. ([#9400](https://github.com/crystal-lang/crystal/pull/9400), thanks @asterite) + +### Concurrency + +- **(breaking-change)** Drop `Concurrent::Future` and top-level methods `delay`, `future`, `lazy`. Use [crystal-community/future.cr](https://github.com/crystal-community/future.cr). ([#9093](https://github.com/crystal-lang/crystal/pull/9093), thanks @bcardiff) + +### System + +- **(breaking-change)** Deprecate `Process#kill`, use `Process#signal`. ([#9006](https://github.com/crystal-lang/crystal/pull/9006), thanks @oprypin, @jan-zajic) +- `Process` raises `IO::Error` (or subclasses). ([#9340](https://github.com/crystal-lang/crystal/pull/9340), thanks @waj) +- Add `Process.quote` and fix shell usages in the compiler. ([#9043](https://github.com/crystal-lang/crystal/pull/9043), [#9369](https://github.com/crystal-lang/crystal/pull/9369), thanks @oprypin, @bcardiff) +- Implement `Process` support on Windows. ([#9047](https://github.com/crystal-lang/crystal/pull/9047), [#9021](https://github.com/crystal-lang/crystal/pull/9021), [#9122](https://github.com/crystal-lang/crystal/pull/9122), [#9112](https://github.com/crystal-lang/crystal/pull/9112), [#9149](https://github.com/crystal-lang/crystal/pull/9149), [#9310](https://github.com/crystal-lang/crystal/pull/9310), thanks @oprypin, @RX14, @kubo, @jan-zajic) +- Fix compile-time checking of `dup3`/`clock_gettime` methods definition. ([#9407](https://github.com/crystal-lang/crystal/pull/9407), thanks @asterite) +- Use `Int64` as portable `Process.pid` type. ([#9019](https://github.com/crystal-lang/crystal/pull/9019), thanks @oprypin) + +### Runtime + +- **(breaking-change)** Deprecate top-level `fork`. ([#9136](https://github.com/crystal-lang/crystal/pull/9136), thanks @bcardiff) +- **(breaking-change)** Move `Debug` to `Crystal` namespace. ([#9176](https://github.com/crystal-lang/crystal/pull/9176), thanks @bcardiff) +- Fix segfaults when static linking with musl. ([#9238](https://github.com/crystal-lang/crystal/pull/9238), thanks @waj) +- Allow calling `at_exit` inside `at_exit`. ([#9388](https://github.com/crystal-lang/crystal/pull/9388), thanks @asterite) +- Rework DWARF loading and fix empty backtraces in musl. ([#9267](https://github.com/crystal-lang/crystal/pull/9267), thanks @waj) +- Add DragonFly(BSD) support. ([#9178](https://github.com/crystal-lang/crystal/pull/9178), thanks @mneumann) +- Add `Crystal::System::Process` to split out system-specific implementations. ([#9035](https://github.com/crystal-lang/crystal/pull/9035), thanks @oprypin) +- Move internal `CallStack` to `Exception::CallStack`. ([#9076](https://github.com/crystal-lang/crystal/pull/9076), thanks @bcardiff) +- Specify pkg-config name for `libevent`. ([#9395](https://github.com/crystal-lang/crystal/pull/9395), thanks @jhass) + +### Spec + +- Reference global Spec in `be_a` macro. ([#9066](https://github.com/crystal-lang/crystal/pull/9066), thanks @asterite) +- Add `-h` short flag to spec runner. ([#9164](https://github.com/crystal-lang/crystal/pull/9164), thanks @straight-shoota) +- Fix `crystal spec` file paths on Windows. ([#9234](https://github.com/crystal-lang/crystal/pull/9234), thanks @oprypin) +- Refactor spec hooks. ([#9090](https://github.com/crystal-lang/crystal/pull/9090), thanks @straight-shoota) + +## Compiler + +- **(breaking-change)** Improve compiler single-file run syntax to make it shebang-friendly `#!`. ([#9171](https://github.com/crystal-lang/crystal/pull/9171), thanks @RX14) +- **(breaking-change)** Use `Process.quote` for `crystal env` output. ([#9428](https://github.com/crystal-lang/crystal/pull/9428), thanks @MakeNowJust) +- **(breaking-change)** Simplify `Link` annotation handling. ([#8972](https://github.com/crystal-lang/crystal/pull/8972), thanks @RX14) +- Fix parsing of `foo:"bar"` inside call or named tuple. ([#9033](https://github.com/crystal-lang/crystal/pull/9033), thanks @asterite) +- Fix parsing of anonymous splat and block arg. ([#9113](https://github.com/crystal-lang/crystal/pull/9113), thanks @MakeNowJust) +- Fix parsing of `unless` inside macro. ([#9024](https://github.com/crystal-lang/crystal/pull/9024), [#9167](https://github.com/crystal-lang/crystal/pull/9167), thanks @MakeNowJust) +- Fix parsing of `\ ` (backslash + space) inside regex literal to ` ` (space). ([#9079](https://github.com/crystal-lang/crystal/pull/9079), thanks @MakeNowJust) +- Fix parsing of ambiguous '+' and '-'. ([#9194](https://github.com/crystal-lang/crystal/pull/9194), thanks @max-codeware) +- Fix parsing of capitalized named argument. ([#9232](https://github.com/crystal-lang/crystal/pull/9232), thanks @asterite) +- Fix parsing of `{[] of Foo, self.foo}` expressions. ([#9329](https://github.com/crystal-lang/crystal/pull/9329), thanks @MakeNowJust) +- Fix cast fun function pointer to Proc. ([#9287](https://github.com/crystal-lang/crystal/pull/9287), thanks @asterite) +- Make compiler warn on deprecated macros. ([#9343](https://github.com/crystal-lang/crystal/pull/9343), thanks @bcardiff) +- Basic support for Win64 C lib ABI. ([#9387](https://github.com/crystal-lang/crystal/pull/9387), thanks @oprypin) +- Make the compiler able to run on Windows and compile itself. ([#9054](https://github.com/crystal-lang/crystal/pull/9054), [#9062](https://github.com/crystal-lang/crystal/pull/9062), [#9095](https://github.com/crystal-lang/crystal/pull/9095), [#9106](https://github.com/crystal-lang/crystal/pull/9106), [#9307](https://github.com/crystal-lang/crystal/pull/9307), thanks @oprypin, @Sija) +- Add docs regarding `CRYSTAL_OPTS`. ([#9018](https://github.com/crystal-lang/crystal/pull/9018), thanks @straight-shoota) +- Remove `Process.run("which")` from compiler. ([#9141](https://github.com/crystal-lang/crystal/pull/9141), thanks @straight-shoota) +- Refactor type parser. ([#9208](https://github.com/crystal-lang/crystal/pull/9208), thanks @MakeNowJust) +- Refactor & clean-up in compiler. ([#8781](https://github.com/crystal-lang/crystal/pull/8781), [#9195](https://github.com/crystal-lang/crystal/pull/9195), thanks @rhysd, @straight-shoota) +- Refactor `CrystalPath::Error`. ([#9359](https://github.com/crystal-lang/crystal/pull/9359), thanks @straight-shoota) +- Refactor and improvements on spec_helper. ([#9367](https://github.com/crystal-lang/crystal/pull/9367), [#9059](https://github.com/crystal-lang/crystal/pull/9059), [#9393](https://github.com/crystal-lang/crystal/pull/9393), [#9351](https://github.com/crystal-lang/crystal/pull/9351), [#9402](https://github.com/crystal-lang/crystal/pull/9402), thanks @straight-shoota, @jhass, @oprypin) +- Split general ABI specs from x86_64-specific ones, run on every platform. ([#9384](https://github.com/crystal-lang/crystal/pull/9384), thanks @oprypin) + +### Language semantics + +- Fix `RegexLiteral#to_s` output when first character of literal is whitespace. ([#9017](https://github.com/crystal-lang/crystal/pull/9017), thanks @MakeNowJust) +- Fix autocasting in multidispatch. ([#9004](https://github.com/crystal-lang/crystal/pull/9004), thanks @asterite) +- Fix propagation of annotations into other scopes. ([#9125](https://github.com/crystal-lang/crystal/pull/9125), thanks @asterite) +- Fix yield computation inside macro code. ([#9324](https://github.com/crystal-lang/crystal/pull/9324), thanks @asterite) +- Fix incorrect type generated with `as?` when type is a union. ([#9417](https://github.com/crystal-lang/crystal/pull/9417), [#9435](https://github.com/crystal-lang/crystal/pull/9435), thanks @asterite) +- Don't duplicate instance var in inherited generic type. ([#9433](https://github.com/crystal-lang/crystal/pull/9433), thanks @asterite) +- Ensure `type_vars` works for generic modules. ([#9161](https://github.com/crystal-lang/crystal/pull/9161), thanks @toddsundsted) +- Make autocasting work in default values against unions. ([#9366](https://github.com/crystal-lang/crystal/pull/9366), thanks @asterite) +- Skip no closure check for non-Crystal procs. ([#9248](https://github.com/crystal-lang/crystal/pull/9248), thanks @jhass) + +### Debugger + +- Improve debugging support. ([#8538](https://github.com/crystal-lang/crystal/pull/8538), thanks @skuznetsov) +- Move recent additions to `DIBuilder` to `LLVMExt`. ([#9114](https://github.com/crystal-lang/crystal/pull/9114), thanks @bcardiff) + +## Tools + +### Formatter + +- Fix formatting of regex after some comments. ([#9109](https://github.com/crystal-lang/crystal/pull/9109), thanks @MakeNowJust) +- Fix formatting of `&.!`. ([#9391](https://github.com/crystal-lang/crystal/pull/9391), thanks @MakeNowJust) +- Avoid crash on heredoc with interpolations. ([#9382](https://github.com/crystal-lang/crystal/pull/9382), thanks @MakeNowJust) +- Refactor: code clean-up. ([#9231](https://github.com/crystal-lang/crystal/pull/9231), thanks @MakeNowJust) + +### Doc generator + +- Fix links to methods with `String` default values. ([#9200](https://github.com/crystal-lang/crystal/pull/9200), thanks @bcardiff) +- Fix syntax highlighting of heredoc. ([#9396](https://github.com/crystal-lang/crystal/pull/9396), thanks @MakeNowJust) +- Correctly attach docs before annotations to following types. ([#9332](https://github.com/crystal-lang/crystal/pull/9332), thanks @asterite) +- Allow annotations and `:ditto:` in macro. ([#9341](https://github.com/crystal-lang/crystal/pull/9341), thanks @bcardiff) +- Add project name and version to API docs. ([#8792](https://github.com/crystal-lang/crystal/pull/8792), thanks @straight-shoota) +- Add version selector to API docs. ([#9074](https://github.com/crystal-lang/crystal/pull/9074), [#9187](https://github.com/crystal-lang/crystal/pull/9187), [#9250](https://github.com/crystal-lang/crystal/pull/9250), [#9252](https://github.com/crystal-lang/crystal/pull/9252), [#9254](https://github.com/crystal-lang/crystal/pull/9254), thanks @straight-shoota, @bcardiff) +- Show input type path instead of full qualified path on generic. ([#9302](https://github.com/crystal-lang/crystal/pull/9302), thanks @MakeNowJust) +- Remove README link in API docs. ([#9082](https://github.com/crystal-lang/crystal/pull/9082), thanks @straight-shoota) +- Remove special handling for version tags in docs generator. ([#9083](https://github.com/crystal-lang/crystal/pull/9083), thanks @straight-shoota) +- Refactor `is_crystal_repo` based on project name. ([#9070](https://github.com/crystal-lang/crystal/pull/9070), thanks @straight-shoota) +- Refactor `Docs::Generator` source link generation. ([#9119](https://github.com/crystal-lang/crystal/pull/9119), [#9305](https://github.com/crystal-lang/crystal/pull/9305), thanks @straight-shoota) + +### Playground + +- Allow building compiler without 'playground', to avoid dependency on sockets. ([#9031](https://github.com/crystal-lang/crystal/pull/9031), thanks @oprypin) +- Add support to jquery version 3. ([#9028](https://github.com/crystal-lang/crystal/pull/9028), thanks @deiv) + +## Others + +- CI improvements and housekeeping. ([#9012](https://github.com/crystal-lang/crystal/pull/9012), [#9129](https://github.com/crystal-lang/crystal/pull/9129), [#9242](https://github.com/crystal-lang/crystal/pull/9242), [#9370](https://github.com/crystal-lang/crystal/pull/9370), thanks @bcardiff) +- Update to Shards 0.11.1. ([#9446](https://github.com/crystal-lang/crystal/pull/9446), thanks @bcardiff) +- Tidy up Makefile and crystal env output. ([#9423](https://github.com/crystal-lang/crystal/pull/9423), thanks @bcardiff) +- Always include `lib` directory in the `CRYSTAL_PATH`. ([#9315](https://github.com/crystal-lang/crystal/pull/9315), thanks @waj) +- Use `SOURCE_DATE_EPOCH` only to determine compiler date. ([#9088](https://github.com/crystal-lang/crystal/pull/9088), thanks @straight-shoota) +- Win CI: Bootstrap Crystal, build things on Windows, publish the binary. ([#9123](https://github.com/crystal-lang/crystal/pull/9123), [#9155](https://github.com/crystal-lang/crystal/pull/9155), [#9144](https://github.com/crystal-lang/crystal/pull/9144), [#9346](https://github.com/crystal-lang/crystal/pull/9346), thanks @oprypin) +- Regenerate implementation tool sample. ([#9003](https://github.com/crystal-lang/crystal/pull/9003), thanks @nulty) +- Avoid requiring non std-lib spec spec_helper. ([#9294](https://github.com/crystal-lang/crystal/pull/9294), thanks @bcardiff) +- Improve grammar and fix typos. ([#9087](https://github.com/crystal-lang/crystal/pull/9087), [#9212](https://github.com/crystal-lang/crystal/pull/9212), [#9368](https://github.com/crystal-lang/crystal/pull/9368), thanks @MakeNowJust, @j8r) +- Hide internal functions in docs. ([#9410](https://github.com/crystal-lang/crystal/pull/9410), thanks @bcardiff) +- Advertise full `crystal spec` command for running a particular spec. ([#9103](https://github.com/crystal-lang/crystal/pull/9103), thanks @paulcsmith) +- Update README. ([#9225](https://github.com/crystal-lang/crystal/pull/9225), [#9163](https://github.com/crystal-lang/crystal/pull/9163), thanks @danimiba, @straight-shoota) +- Fix LICENSE and add NOTICE file. ([#3903](https://github.com/crystal-lang/crystal/pull/3903), thanks @MakeNowJust) + # 0.34.0 (2020-04-06) ## Language changes @@ -241,8 +520,8 @@ - Fixed indent after comment inside indexer. ([#8627](https://github.com/crystal-lang/crystal/pull/8627), thanks @asterite) - Fixed indent of comments at the end of a proc literal. ([#8778](https://github.com/crystal-lang/crystal/pull/8778), thanks @asterite) -- Fixed crash when formating comment after macro. ([#8697](https://github.com/crystal-lang/crystal/pull/8697), thanks @asterite) -- Fixed crash when formating `exp.!`. ([#8768](https://github.com/crystal-lang/crystal/pull/8768), thanks @asterite) +- Fixed crash when formatting comment after macro. ([#8697](https://github.com/crystal-lang/crystal/pull/8697), thanks @asterite) +- Fixed crash when formatting `exp.!`. ([#8768](https://github.com/crystal-lang/crystal/pull/8768), thanks @asterite) - Removes unnecessary escape sequences. ([#8619](https://github.com/crystal-lang/crystal/pull/8619), thanks @RX14) ### Doc generator @@ -3214,7 +3493,7 @@ * Added `IO#gets_to_end`. * Added backticks (`...`) and `%x(...)` for command execution. * Added `%r(...)` for regular expression literals. -* Allow interpolations in regular expresion literals. +* Allow interpolations in regular expression literals. * Compiling with `--release` sets a `release` flag that you can test with `ifdef`. * Allow passing splats to C functions * A C type can now be declared like `type Name = Type` (`type Name : Type` will be deprecated). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd56c948b535..f2e8cece4925 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ the docs execute `make docs`. Please follow the guidelines described in our Be sure to execute `make libcrystal` inside the cloned repository. -Once in the cloned directory, and once you [installed Crystal](http://crystal-lang.org/docs/installation/index.html), +Once in the cloned directory, and once you [installed Crystal](https://crystal-lang.org/install/), you can execute `bin/crystal` instead of `crystal`. This is a wrapper that will use the cloned repository as the standard library. Otherwise the barebones `crystal` executable uses the standard library that comes in your installation. @@ -111,7 +111,7 @@ Then push your changes and create a pull request. ### The compiler itself If you want to add/change something in the compiler, -the first thing you will need to do is to [install the compiler](https://crystal-lang.org/docs/installation/index.html). +the first thing you will need to do is to [install the compiler](https://crystal-lang.org/install/). Once you have a compiler up and running, check that executing `crystal` on the command line prints its usage. Now you can setup your environment to compile Crystal itself, which is itself written in Crystal. Check out diff --git a/Makefile b/Makefile index 607dc23c3137..76b95cb1d032 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,8 @@ ## Run all specs in verbose mode ## $ make spec verbose=1 -LLVM_CONFIG ?= ## llvm-config command path to use +CRYSTAL ?= crystal ## which previous crystal compiler use +LLVM_CONFIG ?= ## llvm-config command path to use release ?= ## Compile in release mode stats ?= ## Enable statistics output @@ -30,11 +31,13 @@ SPEC_SOURCES := $(shell find spec -name '*.cr') override FLAGS += $(if $(release),--release )$(if $(stats),--stats )$(if $(progress),--progress )$(if $(threads),--threads $(threads) )$(if $(debug),-d )$(if $(static),--static )$(if $(LDFLAGS),--link-flags="$(LDFLAGS)" ) SPEC_WARNINGS_OFF := --exclude-warnings spec/std --exclude-warnings spec/compiler SPEC_FLAGS := $(if $(verbose),-v )$(if $(junit_output),--junit_output $(junit_output) ) +CRYSTAL_CONFIG_LIBRARY_PATH := $(shell bin/crystal env CRYSTAL_LIBRARY_PATH 2> /dev/null) CRYSTAL_CONFIG_BUILD_COMMIT := $(shell git rev-parse --short HEAD 2> /dev/null) +SOURCE_DATE_EPOCH := $(shell (git show -s --format=%ct HEAD || stat -c "%Y" Makefile || stat -f "%m" Makefile) 2> /dev/null) EXPORTS := \ - $(if $(release),,CRYSTAL_CONFIG_PATH="$(PWD)/src") \ - CRYSTAL_CONFIG_LIBRARY_PATH="$(shell crystal env CRYSTAL_LIBRARY_PATH)" \ - CRYSTAL_CONFIG_BUILD_COMMIT="$(CRYSTAL_CONFIG_BUILD_COMMIT)" + CRYSTAL_CONFIG_LIBRARY_PATH="$(CRYSTAL_CONFIG_LIBRARY_PATH)" \ + CRYSTAL_CONFIG_BUILD_COMMIT="$(CRYSTAL_CONFIG_BUILD_COMMIT)" \ + SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" SHELL = sh LLVM_CONFIG := $(shell src/llvm/ext/find-llvm-config) LLVM_EXT_DIR = src/llvm/ext @@ -91,7 +94,7 @@ compiler_spec: $(O)/compiler_spec ## Run compiler specs .PHONY: docs docs: ## Generate standard library documentation - $(BUILD_PATH) ./bin/crystal docs src/docs_main.cr $(DOCS_OPTIONS) --project-name=Crystal --project-version=$(CRYSTAL_VERSION) + ./bin/crystal docs src/docs_main.cr $(DOCS_OPTIONS) --project-name=Crystal --project-version=$(CRYSTAL_VERSION) --source-refname=$(CRYSTAL_CONFIG_BUILD_COMMIT) .PHONY: crystal crystal: $(O)/crystal ## Build the compiler @@ -104,19 +107,19 @@ libcrystal: $(LIB_CRYSTAL_TARGET) $(O)/all_spec: $(DEPS) $(SOURCES) $(SPEC_SOURCES) @mkdir -p $(O) - $(EXPORT_CC) $(BUILD_PATH) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/all_spec.cr + $(EXPORT_CC) $(EXPORTS) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/all_spec.cr $(O)/std_spec: $(DEPS) $(SOURCES) $(SPEC_SOURCES) @mkdir -p $(O) - $(EXPORT_CC) $(BUILD_PATH) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/std_spec.cr + $(EXPORT_CC) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/std_spec.cr $(O)/compiler_spec: $(DEPS) $(SOURCES) $(SPEC_SOURCES) @mkdir -p $(O) - $(EXPORT_CC) $(BUILD_PATH) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/compiler_spec.cr + $(EXPORT_CC) $(EXPORTS) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/compiler_spec.cr $(O)/crystal: $(DEPS) $(SOURCES) @mkdir -p $(O) - $(BUILD_PATH) $(EXPORTS) ./bin/crystal build $(FLAGS) -o $@ src/compiler/crystal.cr -D without_openssl -D without_zlib + $(EXPORTS) ./bin/crystal build $(FLAGS) -o $@ src/compiler/crystal.cr -D without_openssl -D without_zlib $(LLVM_EXT_OBJ): $(LLVM_EXT_DIR)/llvm_ext.cc $(CXX) -c $(CXXFLAGS) -o $@ $< $(shell $(LLVM_CONFIG) --cxxflags) diff --git a/README.md b/README.md index 3dac5a0b596f..b93265724db2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Travis CI Build Status](https://travis-ci.org/crystal-lang/crystal.svg)](https://travis-ci.org/crystal-lang/crystal) [![CircleCI Build Status](https://circleci.com/gh/crystal-lang/crystal/tree/master.svg?style=shield)](https://circleci.com/gh/crystal-lang/crystal) [![Join the chat at https://gitter.im/crystal-lang/crystal](https://badges.gitter.im/crystal-lang/crystal.svg)](https://gitter.im/crystal-lang/crystal) -[![Bountysource](https://api.bountysource.com/badge/team?team_id=89730&style=raised)](https://salt.bountysource.com/teams/crystal-lang) [![Code Triagers Badge](https://www.codetriage.com/crystal-lang/crystal/badges/users.svg)](https://www.codetriage.com/crystal-lang/crystal) --- @@ -38,7 +37,7 @@ Project Status Crystal is still under heavy development. There can be breaking changes but we're trying to keep them as minimum as possible. -The development is possible thanks to the community's effort, [84codes](https://www.84codes.com/)' support, and every [BountySource supporter](https://crystal-lang.org/sponsors). +The development is possible thanks to the community's effort and the continued support of [84codes](https://www.84codes.com/), [Nikola Motor Company](https://nikolamotor.com/) and every other [sponsor](https://crystal-lang.org/sponsors). Installing ---------- @@ -65,6 +64,8 @@ Questions or suggestions? Ask on the [Crystal Forum](https://forum.crystal-lang. Contributing ------------ +The Crystal repository is hosted at [crystal-lang/crystal](https://github.com/crystal-lang/crystal) on GitHub. + Read the general [Contributing guide](https://github.com/crystal-lang/crystal/blob/master/CONTRIBUTING.md), and then: 1. Fork it () diff --git a/bin/ci b/bin/ci index 9a3bfacc108e..9ca230c12048 100755 --- a/bin/ci +++ b/bin/ci @@ -46,12 +46,16 @@ on_os() { os="$1" shift - verify_environment + if [ -z "$CI_NIX_SHELL" ]; then + verify_environment - if [ "$TRAVIS_OS_NAME" = "$os" ]; then - echo "${@}" - eval "${@}" - return $? + if [ "$TRAVIS_OS_NAME" = "$os" ]; then + echo "${@}" + eval "${@}" + return $? + else + return 0 + fi else return 0 fi @@ -65,11 +69,32 @@ on_osx() { fail_on_error on_os "osx" "${@}" } +on_nix_shell_eval() { + if [ -n "$CI_NIX_SHELL" ]; then + echo "${@}" + eval "${@}" + return $? + else + return 0 + fi +} + +on_nix_shell() { + fail_on_error on_nix_shell_eval "${@}" +} + +on_github() { + if [ "$GITHUB_ACTIONS" = "true" ]; then + eval "${@}" + return $? + else + return 0 + fi +} + prepare_system() { on_linux 'echo '"'"'{"ipv6":true, "fixed-cidr-v6":"2001:db8:1::/64"}'"'"' | sudo tee /etc/docker/daemon.json' on_linux sudo service docker restart - - on_osx brew update } build() { @@ -93,7 +118,7 @@ build() { with_build_env 'make docs threads=1' ;; *) - with_build_env 'make crystal std_spec compiler_spec docs threads=1 junit_output=.junit/spec.xml' + with_build_env 'make crystal std_spec compiler_spec docs threads=1 junit_output=.junit/spec.xml DOCS_OPTIONS="--json-config-url=/api/versions.json"' ;; esac @@ -108,12 +133,11 @@ format() { prepare_build() { on_linux verify_linux_environment - on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/0.34.0/crystal-0.34.0-1-darwin-x86_64.tar.gz -o ~/crystal.tar.gz - on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-0.34.0-1 crystal;popd' + on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/0.35.1/crystal-0.35.1-1-darwin-x86_64.tar.gz -o ~/crystal.tar.gz + on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-0.35.1-1 crystal;popd' + on_osx brew update --preinstall + on_osx brew bundle --no-lock - on_osx 'brew unlink python@2 || true' - on_osx brew install z3 llvm@10 gmp libevent pcre openssl pkg-config - on_osx brew link --force llvm@10 # Note: brew link --force might show: # Warning: Refusing to link macOS-provided software: llvm # @@ -137,13 +161,14 @@ verify_version() { with_build_env() { command="$1" + on_github "echo '::group::$1'" # Ensure non GMT timezone export TZ="America/New_York" on_linux verify_linux_environment - export DOCKER_TEST_PREFIX="${DOCKER_TEST_PREFIX:=crystallang/crystal:0.34.0}" + export DOCKER_TEST_PREFIX="${DOCKER_TEST_PREFIX:=crystallang/crystal:0.35.1}" case $ARCH in x86_64) @@ -165,6 +190,7 @@ with_build_env() { -v /etc/group:/etc/group \ -w /mnt \ -e CRYSTAL_CACHE_DIR="/tmp/crystal" \ + -e SPEC_SPLIT_DOTS \ "$DOCKER_TEST_IMAGE" \ "$ARCH_CMD" /bin/sh -c "'$command'" @@ -174,6 +200,9 @@ with_build_env() { CRYSTAL_CACHE_DIR="/tmp/crystal" \ /bin/sh -c "'$command'" + on_nix_shell nix-shell --pure $CI_NIX_SHELL_ARGS --run "'TZ=$TZ $command'" + + on_github echo "::endgroup::" } usage() { diff --git a/bin/crystal b/bin/crystal index a16aa21013e9..ec70846e497d 100755 --- a/bin/crystal +++ b/bin/crystal @@ -138,18 +138,31 @@ SCRIPT_ROOT="$(dirname "$SCRIPT_PATH")" CRYSTAL_ROOT="$(dirname "$SCRIPT_ROOT")" CRYSTAL_DIR="$CRYSTAL_ROOT/.build" -export CRYSTAL_PATH=lib:$CRYSTAL_ROOT/src +export CRYSTAL_PATH="${CRYSTAL_PATH:-lib:$CRYSTAL_ROOT/src}" +if [ -n "${CRYSTAL_PATH##*$CRYSTAL_ROOT/src*}" ]; then + __warning_msg "CRYSTAL_PATH env variable does not contains $CRYSTAL_ROOT/src" +fi + export CRYSTAL_HAS_WRAPPER=true +export CRYSTAL="${CRYSTAL:-"crystal"}" + +if [ -z "$CRYSTAL_CONFIG_LIBRARY_PATH" ]; then + export CRYSTAL_CONFIG_LIBRARY_PATH="$( + export PATH="$(remove_path_item "$(remove_path_item "$PATH" "$SCRIPT_ROOT")" "bin")" + crystal env CRYSTAL_LIBRARY_PATH || echo "" + )" +fi + if [ -x "$CRYSTAL_DIR/crystal" ]; then __warning_msg "Using compiled compiler at ${CRYSTAL_DIR#"$PWD/"}/crystal" exec "$CRYSTAL_DIR/crystal" "$@" -elif ! command -v crystal > /dev/null; then - __error_msg 'You need to have a crystal executable in your path!' +elif ! command -v $CRYSTAL > /dev/null; then + __error_msg 'You need to have a crystal executable in your path! or set CRYSTAL env variable' exit 1 -elif [ "$(command -v crystal)" = "$SCRIPT_PATH" ] || [ "$(command -v crystal)" = "bin/crystal" ]; then +elif [ "$(command -v $CRYSTAL)" = "$SCRIPT_PATH" ] || [ "$(command -v $CRYSTAL)" = "bin/crystal" ]; then export PATH="$(remove_path_item "$(remove_path_item "$PATH" "$SCRIPT_ROOT")" "bin")" exec "$SCRIPT_PATH" "$@" else - exec crystal "$@" + exec $CRYSTAL "$@" fi diff --git a/etc/lldb/crystal_formatters.py b/etc/lldb/crystal_formatters.py index e312f88642b3..856dea7c4f55 100644 --- a/etc/lldb/crystal_formatters.py +++ b/etc/lldb/crystal_formatters.py @@ -22,10 +22,10 @@ def get_child_index(self, name): return int(name.lstrip('[').rstrip(']')) except: return -1 - + def get_child_at_index(self,index): if index >= self.size: - return None + return None try: elementType = self.buffer.type.GetPointeeType() offset = elementType.size * index diff --git a/man/crystal.1 b/man/crystal.1 index 48d50894fbb7..c13644503021 100644 --- a/man/crystal.1 +++ b/man/crystal.1 @@ -176,12 +176,36 @@ Options: .Pp .It Fl -project-name Ar NAME Set the project name. The default value is extracted from shard.yml if available. + In case no default can be found, this option is mandatory. .It Fl -project-version Ar VERSION Set the project version. The default value is extracted from current git commit or shard.yml if available. + In case no default can be found, this option is mandatory. -.It Fl -json_config_url Ar URL +.It Fl -json-config-url Ar URL Set the URL pointing to a config file (used for discovering versions). +.It Fl -source-refname Ar REFNAME +Set source refname (e.g. git tag, commit hash). The default value is extracted from current git commit if available. + +If this option is missing and can't be automatically determined, the generator can't produce source code links. +.It Fl -source-url-pattern Ar URL +Set URL pattern for source code links. The default value is extracted from git remotes ("origin" or first one) if available and the provider's URL pattern is recognized. + +.Pp +Supported replacement tags: +.Pp +.Bl -tag -width "%{refname}" -compact +.It Sy %{refname} +commit reference +.It Sy %{path} +path to source file inside the repository +.It Sy %{filename} +basename of the source file +.It Sy %{line} +line number +.El +.Pp +If this option is missing and can't be automatically determined, the generator can't produce source code links. .It Fl o Ar DIR, Fl -output Ar DIR Set the output directory (default: ./docs). .It Fl b Ar URL, Fl -sitemap-base-url Ar URL diff --git a/samples/2048.cr b/samples/2048.cr index 91668f3d5553..a76eccf9a59b 100644 --- a/samples/2048.cr +++ b/samples/2048.cr @@ -25,7 +25,7 @@ module Screen def self.colorize_for(tile) fg_color, bg_color = TILES[tile] - color = with_color(fg_color) + color = Colorize.with.fore(fg_color) color = color.on(bg_color) if bg_color color.surround do yield diff --git a/samples/pretty_json.cr b/samples/pretty_json.cr index 71d0d8d80971..0fb30599ef65 100644 --- a/samples/pretty_json.cr +++ b/samples/pretty_json.cr @@ -21,23 +21,23 @@ class PrettyPrinter def read_any case @pull.kind when .null? - with_color.bold.surround(@output) do + Colorize.with.bold.surround(@output) do @pull.read_null.to_json(@output) end when .bool? - with_color.light_blue.surround(@output) do + Colorize.with.light_blue.surround(@output) do @pull.read_bool.to_json(@output) end when .int? - with_color.red.surround(@output) do + Colorize.with.red.surround(@output) do @pull.read_int.to_json(@output) end when .float? - with_color.red.surround(@output) do + Colorize.with.red.surround(@output) do @pull.read_float.to_json(@output) end when .string? - with_color.yellow.surround(@output) do + Colorize.with.yellow.surround(@output) do @pull.read_string.to_json(@output) end when .begin_array? @@ -80,7 +80,7 @@ class PrettyPrinter print '\n' if @indent > 0 end print_indent - with_color.cyan.surround(@output) do + Colorize.with.cyan.surround(@output) do key.to_json(@output) end print ": " diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000000..00f6bd7f695d --- /dev/null +++ b/shell.nix @@ -0,0 +1,151 @@ +# This nix-shell script can be used to get a complete development environment +# for the Crystal compiler. +# +# You can choose which llvm version use and, on Linux, choose to use musl. +# +# $ nix-shell --pure +# $ nix-shell --pure --arg llvm 10 +# $ nix-shell --pure --arg llvm 10 --arg musl true +# $ nix-shell --pure --arg llvm 9 +# $ nix-shell --pure --arg llvm 9 --argstr system i686-linux +# ... +# $ nix-shell --pure --arg llvm 6 +# +# If needed, you can use https://app.cachix.org/cache/crystal-ci to avoid building +# packages that are not available in Nix directly. This is mostly useful for musl. +# +# $ nix-env -iA cachix -f https://cachix.org/api/v1/install +# $ cachix use crystal-ci +# $ nix-shell --pure --arg musl true +# + +{llvm ? 10, musl ? false, system ? builtins.currentSystem}: + +let + nixpkgs = import (builtins.fetchTarball { + name = "nixpkgs-20.03"; + url = "https://github.com/NixOS/nixpkgs/archive/2d580cd2793a7b5f4b8b6b88fb2ccec700ee1ae6.tar.gz"; + sha256 = "1nbanzrir1y0yi2mv70h60sars9scwmm0hsxnify2ldpczir9n37"; + }) { + inherit system; + }; + + pkgs = if musl then nixpkgs.pkgsMusl else nixpkgs; + + genericBinary = { url, sha256 }: + pkgs.stdenv.mkDerivation rec { + name = "crystal-binary"; + src = builtins.fetchTarball { inherit url sha256; }; + + # Extract only the compiler binary + buildCommand = '' + mkdir -p $out/bin + + # Darwin packages use embedded/bin/crystal + [ ! -f "${src}/embedded/bin/crystal" ] || cp ${src}/embedded/bin/crystal $out/bin/ + + # Linux packages use lib/crystal/bin/crystal + [ ! -f "${src}/lib/crystal/bin/crystal" ] || cp ${src}/lib/crystal/bin/crystal $out/bin/ + ''; + }; + + # Hashes obtained using `nix-prefetch-url --unpack ` + latestCrystalBinary = genericBinary ({ + x86_64-darwin = { + url = "https://github.com/crystal-lang/crystal/releases/download/0.35.1/crystal-0.35.1-1-darwin-x86_64.tar.gz"; + sha256 = "sha256:0gpn42xh372hw2bqfgxc9wibpbam8gn7gx3p1b8p9adydjg0zxfm"; + }; + + x86_64-linux = { + url = "https://github.com/crystal-lang/crystal/releases/download/0.35.1/crystal-0.35.1-1-linux-x86_64.tar.gz"; + sha256 = "sha256:077pby4ylf0z831gg0hbiwxcq3g0yl0cdlybirgg8rv24a2sa7zh"; + }; + + i686-linux = { + url = "https://github.com/crystal-lang/crystal/releases/download/0.35.1/crystal-0.35.1-1-linux-i686.tar.gz"; + sha256 = "sha256:0nfgxjndfslyacicjy4303pvvqfg74v5fnpr4b10ss9rqakmlbgd"; + }; + }.${pkgs.stdenv.system}); + + pkgconfig = pkgs.pkgconfig; + + llvm_suite = ({ + llvm_10 = { + llvm = pkgs.llvm_10; + extra = [ pkgs.lld_10 pkgs.lldb_10 ]; + }; + llvm_9 = { + llvm = pkgs.llvm_9; + extra = [ ]; # lldb it fails to compile on Darwin + }; + llvm_8 = { + llvm = pkgs.llvm_8; + extra = [ ]; # lldb it fails to compile on Darwin + }; + llvm_7 = { + llvm = pkgs.llvm; + extra = [ pkgs.lldb ]; + }; + llvm_6 = { + llvm = pkgs.llvm_6; + extra = [ ]; # lldb it fails to compile on Darwin + }; + }."llvm_${toString llvm}"); + + libatomic_ops = builtins.fetchurl { + url = "https://github.com/ivmai/libatomic_ops/releases/download/v7.6.10/libatomic_ops-7.6.10.tar.gz"; + sha256 = "1bwry043f62pc4mgdd37zx3fif19qyrs8f5bw7qxlmkzh5hdyzjq"; + }; + + boehmgc = pkgs.stdenv.mkDerivation rec { + pname = "boehm-gc"; + version = "8.0.4"; + + src = builtins.fetchTarball { + url = "https://github.com/ivmai/bdwgc/releases/download/v${version}/gc-${version}.tar.gz"; + sha256 = "16ic5dwfw51r5lcl88vx3qrkg3g2iynblazkri3sl9brnqiyzjk7"; + }; + + patches = [ + (pkgs.fetchpatch { + url = "https://github.com/ivmai/bdwgc/commit/5668de71107022a316ee967162bc16c10754b9ce.patch"; + sha256 = "02f0rlxl4fsqk1xiq0pabkhwydnmyiqdik2llygkc6ixhxbii8xw"; + }) + ]; + + postUnpack = '' + mkdir $sourceRoot/libatomic_ops + tar -xzf ${libatomic_ops} -C $sourceRoot/libatomic_ops --strip-components 1 + ''; + + configureFlags = [ + "--disable-debug" + "--disable-dependency-tracking" + "--disable-shared" + "--enable-large-config" + ]; + + enableParallelBuilding = true; + }; + + stdLibDeps = with pkgs; [ + boehmgc gmp libevent libiconv libxml2 libyaml openssl pcre zlib + ] ++ stdenv.lib.optionals stdenv.isDarwin [ libiconv ]; + + tools = [ pkgs.hostname pkgs.git llvm_suite.extra ]; +in + +pkgs.stdenv.mkDerivation rec { + name = "crystal-dev"; + + buildInputs = tools ++ stdLibDeps ++ [ + latestCrystalBinary + pkgconfig + llvm_suite.llvm + ]; + + LLVM_CONFIG = "${llvm_suite.llvm}/bin/llvm-config"; + + # ld: warning: object file (.../src/ext/libcrystal.a(sigfault.o)) was built for newer OSX version (10.14) than being linked (10.12) + MACOSX_DEPLOYMENT_TARGET = "10.11"; +} diff --git a/spec/compiler/codegen/automatic_cast.cr b/spec/compiler/codegen/automatic_cast.cr index 0de8d9eef7d4..16dc08af8f08 100644 --- a/spec/compiler/codegen/automatic_cast.cr +++ b/spec/compiler/codegen/automatic_cast.cr @@ -239,4 +239,35 @@ describe "Code gen: automatic cast" do foo(1, "a" || 1) )).to_i.should eq(20) end + + it "does multidispatch with automatic casting (3)" do + run(%( + abstract class Foo + end + + class Bar < Foo + def foo(x : UInt8) + 2 + end + end + + class Baz < Foo + def foo(x : UInt8) + 3 + end + end + + Bar.new.as(Foo).foo(1) + )).to_i.should eq(2) + end + + it "doesn't autocast number on union (#8655)" do + run(%( + def foo(x : UInt8 | Int32, y : Float64) + x + end + + foo(255, 60) + )).to_i.should eq(255) + end end diff --git a/spec/compiler/codegen/c_abi/c_abi_spec.cr b/spec/compiler/codegen/c_abi/c_abi_spec.cr new file mode 100644 index 000000000000..0cbeb5ecd765 --- /dev/null +++ b/spec/compiler/codegen/c_abi/c_abi_spec.cr @@ -0,0 +1,248 @@ +require "../../../spec_helper" + +describe "Code gen: C ABI" do + it "passes struct less than 64 bits (for real)" do + test_c( + %( + struct s { + char x; + short y; + }; + + int foo(struct s a) { + return a.x + a.y; + } + ), + %( + lib LibFoo + struct Struct + x : Int8 + y : Int16 + end + + fun foo(s : Struct) : Int32 + end + + s = LibFoo::Struct.new x: 1_i8, y: 2_i16 + LibFoo.foo(s) + ), &.to_i.should eq(3)) + end + + it "passes struct between 64 and 128 bits (for real)" do + test_c( + %( + struct s { + long long x; + short y; + }; + + long long foo(struct s a) { + return a.x + a.y; + } + ), + %( + lib LibFoo + struct Struct + x : Int64 + y : Int16 + end + + fun foo(s : Struct) : Int64 + end + + s = LibFoo::Struct.new x: 1_i64, y: 2_i16 + LibFoo.foo(s) + ), &.to_i.should eq(3)) + end + + it "passes struct bigger than 128 bits (for real)" do + test_c( + %( + struct s { + long long x; + long long y; + char z; + }; + + long long foo(struct s a) { + return a.x + a.y + a.z; + } + ), + %( + lib LibFoo + struct Struct + x : Int64 + y : Int64 + z : Int8 + end + + fun foo(s : Struct) : Int64 + end + + s = LibFoo::Struct.new x: 1_i64, y: 2_i64, z: 3_i8 + LibFoo.foo(s) + ), &.to_i.should eq(6)) + end + + {% if flag?(:x86_64) && !flag?(:win32) %} + pending "passes struct after many other args (for real) (#9519)" + {% else %} + it "passes struct after many other args (for real)" do + test_c( + %( + struct s { + long long x, y; + }; + + long long foo(long long a, long long b, long long c, long long d, long long e, struct s v) { + return a + b + c + d + e + v.x + v.y; + } + ), + %( + lib LibFoo + struct S + x : Int64 + y : Int64 + end + + fun foo(a : Int64, b : Int64, c : Int64, d : Int64, e : Int64, v : S) : Int64 + end + + v = LibFoo::S.new(x: 6, y: 7) + LibFoo.foo(1, 2, 3, 4, 5, v) + ), &.to_string.should eq("28")) + end + {% end %} + + it "returns struct less than 64 bits (for real)" do + test_c( + %( + struct s { + char x; + short y; + }; + + struct s foo() { + struct s a = {1, 2}; + return a; + } + ), + %( + lib LibFoo + struct Struct + x : Int8 + y : Int16 + end + + fun foo : Struct + end + + str = LibFoo.foo + str.x.to_i + str.y.to_i + ), &.to_i.should eq(3)) + end + + it "returns struct between 64 and 128 bits (for real)" do + test_c( + %( + struct s { + long long x; + short y; + }; + + struct s foo() { + struct s a = {1, 2}; + return a; + } + ), + %( + lib LibFoo + struct Struct + x : Int64 + y : Int16 + end + + fun foo : Struct + end + + str = LibFoo.foo + (str.x + str.y).to_i32 + ), &.to_i.should eq(3)) + end + + it "returns struct bigger than 128 bits with sret" do + test_c( + %( + struct s { + long long x; + long long y; + char z; + }; + + struct s foo(int z) { + struct s a = {1, 2, z}; + return a; + } + ), + %( + lib LibFoo + struct Struct + x : Int64 + y : Int64 + z : Int8 + end + + fun foo(w : Int32) : Struct + end + + str = LibFoo.foo(3) + (str.x + str.y + str.z).to_i32 + ), &.to_i.should eq(6)) + end + + it "promotes variadic args (float to double)" do + test_c( + %( + #include + + double foo(int n, ...) { + va_list args; + va_start(args, n); + return va_arg(args, double); + } + ), + %( + lib LibFoo + fun foo(n : Int32, ...) : Float64 + end + + LibFoo.foo(1, 1.0_f32) + ), &.to_f64.should eq(1.0)) + end + + [{"i8", -123}, + {"u8", 255}, + {"i16", -123}, + {"u16", 65535}, + ].each do |int_kind, int_value| + it "promotes variadic args (#{int_kind} to i32) (#9742)" do + test_c( + %( + #include + + int foo(int n, ...) { + va_list args; + va_start(args, n); + return va_arg(args, int); + } + ), + %( + lib LibFoo + fun foo(n : Int32, ...) : Int32 + end + + LibFoo.foo(1, #{int_value}_#{int_kind}) + ), &.to_i.should eq(int_value)) + end + end +end diff --git a/spec/compiler/codegen/c_abi/c_abi_x86_64_spec.cr b/spec/compiler/codegen/c_abi/c_abi_x86_64_spec.cr index e01c7189b959..84a92e7075f6 100644 --- a/spec/compiler/codegen/c_abi/c_abi_x86_64_spec.cr +++ b/spec/compiler/codegen/c_abi/c_abi_x86_64_spec.cr @@ -1,6 +1,6 @@ require "../../../spec_helper" -{% if flag?(:x86_64) %} +{% if flag?(:x86_64) && !flag?(:win32) %} describe "Code gen: C ABI x86_64" do it "passes struct less than 64 bits as { i64 }" do mod = codegen(%( @@ -21,33 +21,6 @@ require "../../../spec_helper" str.should contain("declare void @foo({ i64 })") end - it "passes struct less than 64 bits (for real)" do - test_c( - %( - struct s { - char x; - short y; - }; - - int foo(struct s a) { - return a.x + a.y; - } - ), - %( - lib LibFoo - struct Struct - x : Int8 - y : Int16 - end - - fun foo(s : Struct) : Int32 - end - - s = LibFoo::Struct.new x: 1_i8, y: 2_i16 - LibFoo.foo(s) - ), &.to_i.should eq(3)) - end - it "passes struct less than 64 bits as { i64 } in varargs" do mod = codegen(%( lib LibFoo @@ -109,33 +82,6 @@ require "../../../spec_helper" )) end - it "passes struct between 64 and 128 bits (for real)" do - test_c( - %( - struct s { - long x; - short y; - }; - - long foo(struct s a) { - return a.x + a.y; - } - ), - %( - lib LibFoo - struct Struct - x : Int64 - y : Int16 - end - - fun foo(s : Struct) : Int64 - end - - s = LibFoo::Struct.new x: 1_i64, y: 2_i16 - LibFoo.foo(s) - ), &.to_i.should eq(3)) - end - it "passes struct bigger than128 bits with byval" do mod = codegen(%( lib LibFoo @@ -155,35 +101,6 @@ require "../../../spec_helper" str.scan(/byval/).size.should eq(2) end - it "passes struct bigger than128 bits (for real)" do - test_c( - %( - struct s { - long x; - long y; - char z; - }; - - long foo(struct s a) { - return a.x + a.y + a.z; - } - ), - %( - lib LibFoo - struct Struct - x : Int64 - y : Int64 - z : Int8 - end - - fun foo(s : Struct) : Int64 - end - - s = LibFoo::Struct.new x: 1_i64, y: 2_i64, z: 3_i8 - LibFoo.foo(s) - ), &.to_i.should eq(6)) - end - it "returns struct less than 64 bits as { i64 }" do mod = codegen(%( lib LibFoo @@ -202,34 +119,6 @@ require "../../../spec_helper" str.should contain("declare { i64 } @foo()") end - it "returns struct less than 64 bits (for real)" do - test_c( - %( - struct s { - char x; - short y; - }; - - struct s foo() { - struct s a = {1, 2}; - return a; - } - ), - %( - lib LibFoo - struct Struct - x : Int8 - y : Int16 - end - - fun foo : Struct - end - - str = LibFoo.foo - str.x.to_i + str.y.to_i - ), &.to_i.should eq(3)) - end - it "returns struct between 64 and 128 bits as { i64, i64 }" do mod = codegen(%( lib LibFoo @@ -248,34 +137,6 @@ require "../../../spec_helper" str.should contain("declare { i64, i64 } @foo()") end - it "returns struct between 64 and 128 bits (for real)" do - test_c( - %( - struct s { - long x; - short y; - }; - - struct s foo() { - struct s a = {1, 2}; - return a; - } - ), - %( - lib LibFoo - struct Struct - x : Int64 - y : Int16 - end - - fun foo : Struct - end - - str = LibFoo.foo - (str.x + str.y).to_i32 - ), &.to_i.should eq(3)) - end - it "returns struct bigger than 128 bits with sret" do mod = codegen(%( lib LibFoo @@ -294,35 +155,5 @@ require "../../../spec_helper" str.scan(/sret/).size.should eq(2) str.should contain("sret, i32") # sret goes as first argument end - - it "returns struct bigger than 128 bits with sret" do - test_c( - %( - struct s { - long x; - long y; - char z; - }; - - struct s foo(int z) { - struct s a = {1, 2, z}; - return a; - } - ), - %( - lib LibFoo - struct Struct - x : Int64 - y : Int64 - z : Int8 - end - - fun foo(w : Int32) : Struct - end - - str = LibFoo.foo(3) - (str.x + str.y + str.z).to_i32 - ), &.to_i.should eq(6)) - end end {% end %} diff --git a/spec/compiler/codegen/c_struct_spec.cr b/spec/compiler/codegen/c_struct_spec.cr index 4d93e967b3f5..93e16018052c 100644 --- a/spec/compiler/codegen/c_struct_spec.cr +++ b/spec/compiler/codegen/c_struct_spec.cr @@ -379,4 +379,19 @@ describe "Code gen: struct" do foo.x.call(1) )).to_i.should eq(2) end + + it "can access member of uninitialized struct behind type (#8774)" do + run(%( + lib LibFoo + struct Foo + x : Int32 + end + + type FooT = Foo + end + + foo = uninitialized LibFoo::FooT + foo.x + )) + end end diff --git a/spec/compiler/codegen/closure_spec.cr b/spec/compiler/codegen/closure_spec.cr index 15f2d1cf92d4..784b4e2cfe49 100644 --- a/spec/compiler/codegen/closure_spec.cr +++ b/spec/compiler/codegen/closure_spec.cr @@ -681,4 +681,48 @@ describe "Code gen: closure" do f1.call &+ f2.call )) end + + it "ensures it can raise from the closure check" do + expect_raises(Exception, "::raise must be of NoReturn return type!") do + codegen(%( + def raise(m : String) + end + + fun a(a : -> Int32) + end + + value = 1 + p = ->{ value } + a(p) + )) + end + end + + it "allows passing an external function along" do + codegen(%( + require "prelude" + + + lib LibA + fun a(a : Void* -> Void*) + end + + fun b(a : Void* -> Void*) + LibA.a(a) + end + )) + end + + it "allows passing an external function along (2)" do + codegen(%( + lib LibFoo + struct S + callback : -> + end + end + + s = LibFoo::S.new + s.callback = nil + )) + end end diff --git a/spec/compiler/codegen/experimental_spec.cr b/spec/compiler/codegen/experimental_spec.cr new file mode 100644 index 000000000000..ddf66bd65b8f --- /dev/null +++ b/spec/compiler/codegen/experimental_spec.cr @@ -0,0 +1,50 @@ +require "../spec_helper" + +describe "Code gen: experimental" do + it "compiles with no argument" do + run(%( + @[Experimental] + def foo + end + + 2 + )).to_i.should eq(2) + end + + it "compiles with single string argument" do + run(%( + @[Experimental("lorem ipsum")] + def foo + end + + 2 + )).to_i.should eq(2) + end + + it "errors if invalid argument type" do + assert_error %( + @[Experimental(42)] + def foo + end + ), + "Error: first argument must be a String" + end + + it "errors if too many arguments" do + assert_error %( + @[Experimental("lorem ipsum", "extra arg")] + def foo + end + ), + "Error: wrong number of experimental annotation arguments (given 2, expected 1)" + end + + it "errors if missing link arguments" do + assert_error %( + @[Experimental(invalid: "lorem ipsum")] + def foo + end + ), + "Error: too many named arguments (given 1, expected maximum 0)" + end +end diff --git a/spec/compiler/codegen/extern_spec.cr b/spec/compiler/codegen/extern_spec.cr index 535358943e37..6efd7a44c16b 100644 --- a/spec/compiler/codegen/extern_spec.cr +++ b/spec/compiler/codegen/extern_spec.cr @@ -427,13 +427,13 @@ describe "Codegen: extern struct" do ), &.to_i.should eq(30)) end - it "codegens proc that takes and returns an extern struct with sret" do + pending_win32 "codegens proc that takes and returns an extern struct with sret" do test_c( %( struct Struct { - long x; - long y; - long z; + long long x; + long long y; + long long z; }; struct Struct foo(struct Struct (*callback)(struct Struct)) { diff --git a/spec/compiler/codegen/generic_class_spec.cr b/spec/compiler/codegen/generic_class_spec.cr index 27172c71f40d..682eca0b5a0f 100644 --- a/spec/compiler/codegen/generic_class_spec.cr +++ b/spec/compiler/codegen/generic_class_spec.cr @@ -401,4 +401,22 @@ describe "Code gen: generic class type" do end )) end + + it "doesn't override guessed instance var in generic type if already declared in superclass (#9431)" do + codegen(%( + class Foo + @x = 0 + end + + class Bar(T) < Foo + @x = 0 + end + + class Baz < Bar(Int32) + @valid = true + end + + Baz.new + )) + end end diff --git a/spec/compiler/codegen/lib_spec.cr b/spec/compiler/codegen/lib_spec.cr index e899bcf6d2cf..695a02d6fd6b 100644 --- a/spec/compiler/codegen/lib_spec.cr +++ b/spec/compiler/codegen/lib_spec.cr @@ -15,11 +15,11 @@ describe "Code gen: lib" do it "call to void function" do run(" lib LibC - fun srandom(x : UInt32) : Void + fun srand(x : UInt32) : Void end def foo - LibC.srandom(0_u32) + LibC.srand(0_u32) end foo diff --git a/spec/compiler/codegen/macro_spec.cr b/spec/compiler/codegen/macro_spec.cr index 8ed6dd31a49e..9f3e6e1211d1 100644 --- a/spec/compiler/codegen/macro_spec.cr +++ b/spec/compiler/codegen/macro_spec.cr @@ -578,6 +578,17 @@ describe "Code gen: macro" do )).to_string.should eq("Int32") end + it "can acccess type variables of a module" do + run(%( + module Foo(T) + def self.foo + {{ @type.type_vars.first.name.stringify }} + end + end + Foo(Int32).foo + )).to_string.should eq("Int32") + end + it "can acccess type variables that are not types" do run(%( class Foo(T) diff --git a/spec/compiler/codegen/nilable_cast_spec.cr b/spec/compiler/codegen/nilable_cast_spec.cr index f9b5182efcea..c30ff6fd5c7e 100644 --- a/spec/compiler/codegen/nilable_cast_spec.cr +++ b/spec/compiler/codegen/nilable_cast_spec.cr @@ -147,4 +147,26 @@ describe "Code gen: nilable cast" do x.try &.as?(Foo) )) end + + it "casts union type to nilable type (#9342)" do + run(%( + struct Nil + def foo + 0 + end + end + + class Gen(T) + def initialize(@value : Int32) + end + + def foo + @value + end + end + + a = Gen(String).new(10) || Gen(Int32).new(20) + a.as?(Gen).foo + )).to_i.should eq(10) + end end diff --git a/spec/compiler/codegen/primitives_spec.cr b/spec/compiler/codegen/primitives_spec.cr index fec15c2eeaa9..7d6fba92ec37 100644 --- a/spec/compiler/codegen/primitives_spec.cr +++ b/spec/compiler/codegen/primitives_spec.cr @@ -239,46 +239,49 @@ describe "Code gen: primitives" do end describe "va_arg" do - it "uses llvm's va_arg instruction" do - mod = codegen(%( - struct VaList - @[Primitive(:va_arg)] - def next(type) + # On Windows and AArch64 llvm's va_arg instruction works incorrectly. + {% unless flag?(:win32) || flag?(:aarch64) %} + it "uses llvm's va_arg instruction" do + mod = codegen(%( + struct VaList + @[Primitive(:va_arg)] + def next(type) + end end - end - list = VaList.new - list.next(Int32) - )) - str = mod.to_s - str.should contain("va_arg %VaList* %list") - end - - it "works with C code" do - test_c( - %( - extern int foo_f(int,...); - int foo() { - return foo_f(3,1,2,3); - } - ), - %( - lib LibFoo - fun foo() : LibC::Int - end + list = VaList.new + list.next(Int32) + )) + str = mod.to_s + str.should contain("va_arg %VaList* %list") + end - fun foo_f(count : Int32, ...) : LibC::Int - sum = 0 - VaList.open do |list| - count.times do |i| - sum += list.next(Int32) + it "works with C code" do + test_c( + %( + extern int foo_f(int,...); + int foo() { + return foo_f(3,1,2,3); + } + ), + %( + lib LibFoo + fun foo() : LibC::Int + end + + fun foo_f(count : Int32, ...) : LibC::Int + sum = 0 + VaList.open do |list| + count.times do |i| + sum += list.next(Int32) + end end + sum end - sum - end - LibFoo.foo - ), &.to_i.should eq(6)) - end + LibFoo.foo + ), &.to_i.should eq(6)) + end + {% end %} end end diff --git a/spec/compiler/codegen/private_spec.cr b/spec/compiler/codegen/private_spec.cr index 613d0b0bd162..c2d2452952c7 100644 --- a/spec/compiler/codegen/private_spec.cr +++ b/spec/compiler/codegen/private_spec.cr @@ -14,11 +14,9 @@ describe "Codegen: private" do ] compiler.prelude = "empty" - output_filename = File.tempname("crystal-spec-output") - - compiler.compile sources, output_filename - ensure - File.delete(output_filename) if output_filename + with_temp_executable "crystal-spec-output" do |output_filename| + compiler.compile sources, output_filename + end end it "codegens overloaded private def in same file" do @@ -39,11 +37,9 @@ describe "Codegen: private" do ] compiler.prelude = "empty" - output_filename = File.tempname("crystal-spec-output") - - compiler.compile sources, output_filename - ensure - File.delete(output_filename) if output_filename + with_temp_executable "crystal-spec-output" do |output_filename| + compiler.compile sources, output_filename + end end it "doesn't include filename for private types" do diff --git a/spec/compiler/codegen/proc_spec.cr b/spec/compiler/codegen/proc_spec.cr index 390d49ac235e..2bbe672b0322 100644 --- a/spec/compiler/codegen/proc_spec.cr +++ b/spec/compiler/codegen/proc_spec.cr @@ -819,4 +819,32 @@ describe "Code gen: proc" do a )).to_i.should eq(3) end + + it "calls function pointer" do + run(%( + require "prelude" + + fun foo(f : Int32 -> Int32) : Int32 + f.call(1) + end + + foo(->(x : Int32) { x &+ 1 }) + )).to_i.should eq(2) + end + + it "casts from function pointer to proc" do + codegen(%( + fun a(a : Void* -> Void*) + Pointer(Proc((Void* -> Void*), Void*)).new(0_u64).value.call(a) + end + )) + end + + it "takes pointerof function pointer" do + codegen(%( + fun a(a : Void* -> Void*) + pointerof(a).value.call(Pointer(Void).new(0_u64)) + end + )) + end end diff --git a/spec/compiler/codegen/thread_local_spec.cr b/spec/compiler/codegen/thread_local_spec.cr index 09e40b159bf4..ea26bfe50fcc 100644 --- a/spec/compiler/codegen/thread_local_spec.cr +++ b/spec/compiler/codegen/thread_local_spec.cr @@ -2,7 +2,7 @@ require "../../spec_helper" {% if !flag?(:openbsd) %} describe "Codegen: thread local" do - it "works with class variables" do + pending_win32 "works with class variables" do run(%( require "prelude" diff --git a/spec/compiler/compiler_spec.cr b/spec/compiler/compiler_spec.cr index 56746736b7b1..66d2ad7871aa 100644 --- a/spec/compiler/compiler_spec.cr +++ b/spec/compiler/compiler_spec.cr @@ -7,24 +7,35 @@ describe "Compiler" do end it "compiles a file" do - with_tempfile "compiler_spec_output" do |path| + with_temp_executable "compiler_spec_output" do |path| Crystal::Command.run ["build"].concat(program_flags_options).concat([compiler_datapath("compiler_sample"), "-o", path]) File.exists?(path).should be_true - `#{path}`.should eq("Hello!") + `#{Process.quote(path)}`.should eq("Hello!") end end it "runs subcommand in preference to a filename " do Dir.cd compiler_datapath do - with_tempfile "compiler_spec_output" do |path| + with_temp_executable "compiler_spec_output" do |path| Crystal::Command.run ["build"].concat(program_flags_options).concat(["compiler_sample", "-o", path]) File.exists?(path).should be_true - `#{path}`.should eq("Hello!") + `#{Process.quote(path)}`.should eq("Hello!") end end end + + it "treats all arguments post-filename as program arguments" do + with_tempfile "args_test" do |path| + `bin/crystal #{Process.quote(File.join(compiler_datapath, "args_test"))} -Dother_flag -- bar #{Process.quote(path)}` + + File.read(path).should eq(<<-FILE) + ["-Dother_flag", "--", "bar"] + {other_flag: false} + FILE + end + end end diff --git a/spec/compiler/config_spec.cr b/spec/compiler/config_spec.cr index 79510b22b6e3..77750b33a292 100644 --- a/spec/compiler/config_spec.cr +++ b/spec/compiler/config_spec.cr @@ -2,8 +2,8 @@ require "../spec_helper" require "./spec_helper" describe Crystal::Config do - it ".default_target" do - Crystal::Config.default_target.should eq Crystal::Codegen::Target.new({{ `crystal --version`.lines[-1] }}.lstrip("Default target: ")) + it ".host_target" do + Crystal::Config.host_target.should eq Crystal::Codegen::Target.new({{ `bin/crystal --version`.lines[-1] }}.lchop("Default target: ")) end {% if flag?(:linux) %} diff --git a/spec/compiler/crystal/tools/doc/generator_spec.cr b/spec/compiler/crystal/tools/doc/generator_spec.cr index 53380bb50a75..f659e580e330 100644 --- a/spec/compiler/crystal/tools/doc/generator_spec.cr +++ b/spec/compiler/crystal/tools/doc/generator_spec.cr @@ -1,5 +1,7 @@ require "../../../spec_helper" +private ANNOTATION_COLORS = {"Deprecated" => "red", "Experimental" => "lime"} + describe Doc::Generator do describe "#must_include_toplevel?" do it "returns false if program has nothing" do @@ -99,30 +101,32 @@ describe Doc::Generator do end describe "#formatted_summary" do - describe "with a Deprecated annotation, and no docs" do - it "should generate just the Deprecated tag" do - program = Program.new - generator = Doc::Generator.new program, ["."] - doc_type = Doc::Type.new generator, program - - a_def = Def.new "foo" - a_def.add_annotation(program.deprecated_annotation, Annotation.new(Crystal::Path.new("Deprecated"), ["don't use me".string] of ASTNode)) - doc_method = Doc::Method.new generator, doc_type, a_def, false - doc_method.formatted_summary.should eq %(

DEPRECATED don't use me

\n\n) + ANNOTATION_COLORS.each do |ann, color| + describe "with a #{ann} annotation, and no docs" do + it "should generate just the #{ann} tag" do + program = Program.new + generator = Doc::Generator.new program, ["."] + doc_type = Doc::Type.new generator, program + + a_def = Def.new "foo" + a_def.add_annotation(program.types[ann].as(Crystal::AnnotationType), Annotation.new(Crystal::Path.new(ann), ["lorem ipsum".string] of ASTNode)) + doc_method = Doc::Method.new generator, doc_type, a_def, false + doc_method.formatted_summary.should eq %(

#{ann.upcase} lorem ipsum

\n\n) + end end - end - describe "with a Deprecated annotation, and docs" do - it "should generate both the docs and Deprecated tag" do - program = Program.new - generator = Doc::Generator.new program, ["."] - doc_type = Doc::Type.new generator, program - - a_def = Def.new "foo" - a_def.doc = "Some Method" - a_def.add_annotation(program.deprecated_annotation, Annotation.new(Crystal::Path.new("Deprecated"), ["don't use me".string] of ASTNode)) - doc_method = Doc::Method.new generator, doc_type, a_def, false - doc_method.formatted_summary.should eq %(

Some Method

\n\n

DEPRECATED don't use me

\n\n) + describe "with a #{ann} annotation, and docs" do + it "should generate both the docs and #{ann} tag" do + program = Program.new + generator = Doc::Generator.new program, ["."] + doc_type = Doc::Type.new generator, program + + a_def = Def.new "foo" + a_def.doc = "Some Method" + a_def.add_annotation(program.types[ann].as(Crystal::AnnotationType), Annotation.new(Crystal::Path.new(ann), ["lorem ipsum".string] of ASTNode)) + doc_method = Doc::Method.new generator, doc_type, a_def, false + doc_method.formatted_summary.should eq %(

Some Method

\n\n

#{ann.upcase} lorem ipsum

\n\n) + end end end @@ -162,30 +166,32 @@ describe Doc::Generator do end describe "#formatted_doc" do - describe "with a Deprecated annotation, and no docs" do - it "should generate just the Deprecated tag" do - program = Program.new - generator = Doc::Generator.new program, ["."] - doc_type = Doc::Type.new generator, program - - a_def = Def.new "foo" - a_def.add_annotation(program.deprecated_annotation, Annotation.new(Crystal::Path.new("Deprecated"), ["don't use me".string] of ASTNode)) - doc_method = Doc::Method.new generator, doc_type, a_def, false - doc_method.formatted_doc.should eq %(

DEPRECATED don't use me

\n\n) + ANNOTATION_COLORS.each do |ann, color| + describe "with a #{ann} annotation, and no docs" do + it "should generate just the #{ann} tag" do + program = Program.new + generator = Doc::Generator.new program, ["."] + doc_type = Doc::Type.new generator, program + + a_def = Def.new "foo" + a_def.add_annotation(program.types[ann].as(Crystal::AnnotationType), Annotation.new(Crystal::Path.new(ann), ["lorem ipsum".string] of ASTNode)) + doc_method = Doc::Method.new generator, doc_type, a_def, false + doc_method.formatted_doc.should eq %(

#{ann.upcase} lorem ipsum

\n\n) + end end - end - describe "with a Deprecated annotation, and docs" do - it "should generate both the docs and Deprecated tag" do - program = Program.new - generator = Doc::Generator.new program, ["."] - doc_type = Doc::Type.new generator, program - - a_def = Def.new "foo" - a_def.doc = "Some Method" - a_def.add_annotation(program.deprecated_annotation, Annotation.new(Crystal::Path.new("Deprecated"), ["don't use me".string] of ASTNode)) - doc_method = Doc::Method.new generator, doc_type, a_def, false - doc_method.formatted_doc.should eq %(

Some Method

\n\n

DEPRECATED don't use me

\n\n) + describe "with a #{ann} annotation, and docs" do + it "should generate both the docs and #{ann} tag" do + program = Program.new + generator = Doc::Generator.new program, ["."] + doc_type = Doc::Type.new generator, program + + a_def = Def.new "foo" + a_def.doc = "Some Method" + a_def.add_annotation(program.types[ann].as(Crystal::AnnotationType), Annotation.new(Crystal::Path.new(ann), ["lorem ipsum".string] of ASTNode)) + doc_method = Doc::Method.new generator, doc_type, a_def, false + doc_method.formatted_doc.should eq %(

Some Method

\n\n

#{ann.upcase} lorem ipsum

\n\n) + end end end diff --git a/spec/compiler/crystal/tools/doc/highlighter_spec.cr b/spec/compiler/crystal/tools/doc/highlighter_spec.cr index 2eb188445b14..abce4c82a253 100644 --- a/spec/compiler/crystal/tools/doc/highlighter_spec.cr +++ b/spec/compiler/crystal/tools/doc/highlighter_spec.cr @@ -76,4 +76,36 @@ describe "Crystal::Doc::Highlighter#highlight" do it_highlights "%w(foo bar\n baz)", %(%w(foo bar\n baz)) it_highlights "%w", %(%w<foo bar baz>) it_highlights "%i(foo bar baz)", %(%i(foo bar baz)) + + it_highlights <<-CR, <<-HTML + foo, bar = <<-FOO, <<-BAR + foo + FOO + bar + BAR + CR + foo, bar = <<-FOO, <<-BAR + foo + FOO + bar + BAR + HTML + + it_highlights <<-CR, <<-HTML + foo, bar = <<-FOO, <<-BAR + foo + FOO + CR + foo, bar = <<-FOO, <<-BAR + foo + FOO + HTML + + it_highlights <<-CR, <<-HTML + foo, bar = <<-FOO, <<-BAR + foo + CR + foo, bar = <<-FOO, <<-BAR + foo + HTML end diff --git a/spec/compiler/crystal/tools/doc/project_info_spec.cr b/spec/compiler/crystal/tools/doc/project_info_spec.cr index 577f442ce4d8..42191829968a 100644 --- a/spec/compiler/crystal/tools/doc/project_info_spec.cr +++ b/spec/compiler/crystal/tools/doc/project_info_spec.cr @@ -24,8 +24,8 @@ describe Crystal::Doc::ProjectInfo do describe "#fill_with_defaults" do it "empty folder" do - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new(nil, nil)) - assert_with_defaults(ProjectInfo.new("foo", "1.0"), ProjectInfo.new("foo", "1.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new(nil, nil, refname: nil)) + assert_with_defaults(ProjectInfo.new("foo", "1.0"), ProjectInfo.new("foo", "1.0", refname: nil)) end context "with shard.yml" do @@ -34,60 +34,74 @@ describe Crystal::Doc::ProjectInfo do end it "no git" do - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "1.0")) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) - assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "1.0", refname: nil)) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: nil)) + assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0", refname: nil)) end it "git but no commit" do run_git "init" - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", nil)) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) - assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", nil, refname: nil)) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: nil)) + assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0", refname: nil)) end it "git tagged version" do run_git "init" run_git "add shard.yml" - run_git "commit -m 'Initial commit' --no-gpg-sign" + run_git "commit -m \"Initial commit\" --no-gpg-sign" run_git "tag v3.0" - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "3.0")) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "3.0", refname: "v3.0")) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: "v3.0")) + assert_with_defaults(ProjectInfo.new("bar", "2.0", refname: "12345"), ProjectInfo.new("bar", "2.0", refname: "12345")) end it "git tagged version dirty" do run_git "init" run_git "add shard.yml" - run_git "commit -m 'Initial commit' --no-gpg-sign" + run_git "commit -m \"Initial commit\" --no-gpg-sign" run_git "tag v3.0" File.write("foo.txt", "bar") - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "3.0-dev")) - assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1")) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "3.0-dev", refname: nil)) + assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1", refname: nil)) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: nil)) end it "git non-tagged commit" do run_git "init" run_git "add shard.yml" - run_git "commit -m 'Initial commit' --no-gpg-sign" + run_git "commit -m \"Initial commit\" --no-gpg-sign" + commit_sha = `git rev-parse HEAD`.chomp - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "master")) - assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1")) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "master", refname: commit_sha)) + assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1", refname: commit_sha)) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: commit_sha)) + assert_with_defaults(ProjectInfo.new("bar", "2.0", refname: "12345"), ProjectInfo.new("bar", "2.0", refname: "12345")) end it "git non-tagged commit dirty" do run_git "init" run_git "add shard.yml" - run_git "commit -m 'Initial commit' --no-gpg-sign" + run_git "commit -m \"Initial commit\" --no-gpg-sign" File.write("foo.txt", "bar") - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "master-dev")) - assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1")) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", "master-dev", refname: nil)) + assert_with_defaults(ProjectInfo.new(nil, "1.1"), ProjectInfo.new("foo", "1.1", refname: nil)) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: nil)) + end + + it "git with remote" do + run_git "init" + run_git "remote add origin git@github.com:foo/bar" + + url_pattern = "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new("foo", nil, refname: nil, source_url_pattern: url_pattern)) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: nil, source_url_pattern: url_pattern)) + assert_with_defaults(ProjectInfo.new(nil, "2.0"), ProjectInfo.new("foo", "2.0", refname: nil, source_url_pattern: url_pattern)) + assert_with_defaults(ProjectInfo.new(nil, "2.0", source_url_pattern: "foo_bar"), ProjectInfo.new("foo", "2.0", refname: nil, source_url_pattern: "foo_bar")) end end @@ -95,12 +109,13 @@ describe Crystal::Doc::ProjectInfo do File.write("foo.txt", "bar") run_git "init" run_git "add foo.txt" - run_git "commit -m 'Remove shard.yml' --no-gpg-sign" + run_git "commit -m \"Remove shard.yml\" --no-gpg-sign" run_git "tag v4.0" - assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new(nil, "4.0")) - assert_with_defaults(ProjectInfo.new("foo", nil), ProjectInfo.new("foo", "4.0")) - assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0")) + assert_with_defaults(ProjectInfo.new(nil, nil), ProjectInfo.new(nil, "4.0", refname: "v4.0")) + assert_with_defaults(ProjectInfo.new("foo", nil), ProjectInfo.new("foo", "4.0", refname: "v4.0")) + assert_with_defaults(ProjectInfo.new("bar", "2.0"), ProjectInfo.new("bar", "2.0", refname: "v4.0")) + assert_with_defaults(ProjectInfo.new("bar", "2.0", refname: "12345"), ProjectInfo.new("bar", "2.0", refname: "12345")) end end @@ -115,7 +130,7 @@ describe Crystal::Doc::ProjectInfo do # Non-tagged commit File.write("file.txt", "foo") run_git "add file.txt" - run_git "commit -m 'Initial commit' --no-gpg-sign" + run_git "commit -m \"Initial commit\" --no-gpg-sign" ProjectInfo.find_git_version.should eq "master" # Other branch @@ -149,6 +164,40 @@ describe Crystal::Doc::ProjectInfo do ProjectInfo.find_git_version.should eq "0.1.0" end + describe ".git_remote" do + it "no git workdir" do + ProjectInfo.git_remote.should be_nil + end + + it "no remote" do + run_git "init" + ProjectInfo.git_remote.should be_nil + end + + it "simple origin" do + run_git "init" + run_git "remote add origin https://example.com/foo.git" + ProjectInfo.git_remote.should eq "https://example.com/foo.git" + end + + it "origin plus other" do + run_git "init" + run_git "remote add bar https://example.com/bar.git" + run_git "remote add origin https://example.com/foo.git" + run_git "remote add baz https://example.com/baz.git" + `git remote -v` + ProjectInfo.git_remote.should eq "https://example.com/foo.git" + end + + it "no origin remote" do + run_git "init" + run_git "remote add bar https://example.com/bar.git" + run_git "remote add baz https://example.com/baz.git" + `git remote -v` + ProjectInfo.git_remote.should eq "https://example.com/bar.git" + end + end + describe ".read_shard_properties" do it "no shard.yml" do ProjectInfo.read_shard_properties.should eq({nil, nil}) @@ -202,4 +251,76 @@ describe Crystal::Doc::ProjectInfo do ProjectInfo.read_shard_properties.should eq({nil, nil}) end end + + it ".find_source_url_pattern" do + ProjectInfo.find_source_url_pattern("no a uri").should be_nil + ProjectInfo.find_source_url_pattern("git@example.com:foo/bar").should be_nil + ProjectInfo.find_source_url_pattern("http://example.com/foo/bar").should be_nil + + ProjectInfo.find_source_url_pattern("git@github.com:foo/bar/").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("git@github.com:foo/bar.git").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + + ProjectInfo.find_source_url_pattern("git@github.com:foo/bar").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("http://github.com/foo/bar").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("https://github.com/foo/bar").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("http://www.github.com/foo/bar").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("https://www.github.com/foo/bar").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + + ProjectInfo.find_source_url_pattern("https://github.com/foo/bar.git").should eq "https://github.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("https://github.com/foo/bar.cr").should eq "https://github.com/foo/bar.cr/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("https://github.com/foo/bar.cr.git").should eq "https://github.com/foo/bar.cr/blob/%{refname}/%{path}#L%{line}" + + ProjectInfo.find_source_url_pattern("git@gitlab.com:foo/bar").should eq "https://gitlab.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("http://gitlab.com/foo/bar").should eq "https://gitlab.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("http://gitlab.com/foo/bar.git").should eq "https://gitlab.com/foo/bar/blob/%{refname}/%{path}#L%{line}" + + ProjectInfo.find_source_url_pattern("git@bitbucket.com:foo/bar").should eq "https://bitbucket.com/foo/bar/src/%{refname}/%{path}#%{filename}-%{line}" + ProjectInfo.find_source_url_pattern("http://bitbucket.com/foo/bar").should eq "https://bitbucket.com/foo/bar/src/%{refname}/%{path}#%{filename}-%{line}" + ProjectInfo.find_source_url_pattern("http://bitbucket.com/foo/bar.git").should eq "https://bitbucket.com/foo/bar/src/%{refname}/%{path}#%{filename}-%{line}" + + ProjectInfo.find_source_url_pattern("git@git.sr.ht:~foo/bar").should eq "https://git.sr.ht/~foo/bar/tree/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("http://git.sr.ht/~foo/bar").should eq "https://git.sr.ht/~foo/bar/tree/%{refname}/%{path}#L%{line}" + ProjectInfo.find_source_url_pattern("http://git.sr.ht/~foo/bar.git").should eq "https://git.sr.ht/~foo/bar.git/tree/%{refname}/%{path}#L%{line}" + end + + describe "#source_url" do + it "fails if refname is missing" do + location = Crystal::Doc::RelativeLocation.new("foo/bar.baz", 42) + info = ProjectInfo.new("test", "v1.0", refname: nil, source_url_pattern: "http://git.example.com/test.git/src/%{refname}/%{path}#L%{line}") + info.source_url(location).should be_nil + end + + it "fails if pattern is missing" do + location = Crystal::Doc::RelativeLocation.new("foo/bar.baz", 42) + info = ProjectInfo.new("test", "v1.0", refname: "master") + info.source_url(location).should be_nil + end + + it "builds url" do + info = ProjectInfo.new("test", "v1.0", refname: "master", source_url_pattern: "http://git.example.com/test.git/src/%{refname}/%{path}#L%{line}") + location = Crystal::Doc::RelativeLocation.new("foo/bar.baz", 42) + info.source_url(location).should eq "http://git.example.com/test.git/src/master/foo/bar.baz#L42" + end + + it "returns nil for empty pattern" do + info = ProjectInfo.new("test", "v1.0", refname: "master", source_url_pattern: "") + location = Crystal::Doc::RelativeLocation.new("foo/bar.baz", 42) + info.source_url(location).should be_nil + end + + it "fails if pattern is missing" do + location = Crystal::Doc::RelativeLocation.new("foo/bar.baz", 42) + info = ProjectInfo.new("test", "v1.0") + info.refname = "master" + info.source_url(location).should be_nil + end + + it "builds url" do + info = ProjectInfo.new("test", "v1.0") + info.refname = "master" + info.source_url_pattern = "http://git.example.com/test.git/src/%{refname}/%{path}#L%{line}" + location = Crystal::Doc::RelativeLocation.new("foo/bar.baz", 42) + info.source_url(location).should eq "http://git.example.com/test.git/src/master/foo/bar.baz#L42" + end + end end diff --git a/spec/compiler/crystal/tools/doc/type_spec.cr b/spec/compiler/crystal/tools/doc/type_spec.cr index f9651b4632c7..87a9e7a19bc0 100644 --- a/spec/compiler/crystal/tools/doc/type_spec.cr +++ b/spec/compiler/crystal/tools/doc/type_spec.cr @@ -44,4 +44,58 @@ describe Doc::Type do foo.lookup_class_method("new").should_not be_nil foo.lookup_class_method("new", 1).should_not be_nil end + + describe "#node_to_html" do + it "shows relative path" do + program = semantic(<<-CODE).program + class Foo + class Bar + end + end + CODE + + generator = Doc::Generator.new program, [""] + foo = generator.type(program.types["Foo"]) + foo.node_to_html("Bar".path).should eq(%(Bar)) + end + + it "shows relative generic" do + program = semantic(<<-CODE).program + class Foo + class Bar(T) + end + end + CODE + + generator = Doc::Generator.new program, [""] + foo = generator.type(program.types["Foo"]) + foo.node_to_html(Generic.new("Bar".path, ["Foo".path] of ASTNode)).should eq(%(Bar(Foo))) + end + + it "shows generic path with necessary colons" do + program = semantic(<<-CODE).program + class Foo + class Foo + end + end + CODE + + generator = Doc::Generator.new program, [""] + foo = generator.type(program.types["Foo"]) + foo.node_to_html("Foo".path(global: true)).should eq(%(::Foo)) + end + + it "shows generic path with unnecessary colons" do + program = semantic(<<-CODE).program + class Foo + class Bar + end + end + CODE + + generator = Doc::Generator.new program, [""] + foo = generator.type(program.types["Foo"]) + foo.node_to_html("Foo".path(global: true)).should eq(%(Foo)) + end + end end diff --git a/spec/compiler/crystal/tools/doc_spec.cr b/spec/compiler/crystal/tools/doc_spec.cr index e11fadc95f98..d6acc34b9f29 100644 --- a/spec/compiler/crystal/tools/doc_spec.cr +++ b/spec/compiler/crystal/tools/doc_spec.cr @@ -1,51 +1,6 @@ require "../../../spec_helper" -private def assert_matches_pattern(url, **options) - match = Crystal::Doc::Generator::GIT_REMOTE_PATTERNS.each_key.compact_map(&.match(url)).first? - if match - options.each { |k, v| match[k.to_s].should eq(v) } - end -end - describe Crystal::Doc::Generator do - describe "GIT_REMOTE_PATTERNS" do - it "matches github repos" do - assert_matches_pattern "https://www.github.com/foo/bar", user: "foo", repo: "bar" - assert_matches_pattern "http://www.github.com/foo/bar", user: "foo", repo: "bar" - assert_matches_pattern "http://github.com/foo/bar", user: "foo", repo: "bar" - - assert_matches_pattern "https://github.com/foo/bar", user: "foo", repo: "bar" - assert_matches_pattern "https://github.com/foo/bar.git", user: "foo", repo: "bar" - assert_matches_pattern "https://github.com/foo/bar.cr", user: "foo", repo: "bar.cr" - assert_matches_pattern "https://github.com/foo/bar.cr.git", user: "foo", repo: "bar.cr" - - assert_matches_pattern "origin\thttps://github.com/foo/bar.cr.git (fetch)\n", user: "foo", repo: "bar.cr" - assert_matches_pattern "origin\tgit@github.com/foo/bar.cr.git (fetch)\n", user: "foo", repo: "bar.cr" - - assert_matches_pattern "https://github.com/fOO-Bar/w00den-baRK.ab.cd", user: "fOO-Bar", repo: "w00den-baRK.ab.cd" - assert_matches_pattern "https://github.com/fOO-Bar/w00den-baRK.ab.cd.git", user: "fOO-Bar", repo: "w00den-baRK.ab.cd" - assert_matches_pattern "https://github.com/foo_bar/_baz-buzz.cx", user: "foo_bar", repo: "_baz-buzz.cx" - end - - it "matches gitlab repos" do - assert_matches_pattern "https://www.gitlab.com/foo/bar", user: "foo", repo: "bar" - assert_matches_pattern "http://www.gitlab.com/foo/bar", user: "foo", repo: "bar" - assert_matches_pattern "http://gitlab.com/foo/bar", user: "foo", repo: "bar" - - assert_matches_pattern "https://gitlab.com/foo/bar", user: "foo", repo: "bar" - assert_matches_pattern "https://gitlab.com/foo/bar.git", user: "foo", repo: "bar" - assert_matches_pattern "https://gitlab.com/foo/bar.cr", user: "foo", repo: "bar.cr" - assert_matches_pattern "https://gitlab.com/foo/bar.cr.git", user: "foo", repo: "bar.cr" - - assert_matches_pattern "origin\thttps://gitlab.com/foo/bar.cr.git (fetch)\n", user: "foo", repo: "bar.cr" - assert_matches_pattern "origin\tgit@gitlab.com/foo/bar.cr.git (fetch)\n", user: "foo", repo: "bar.cr" - - assert_matches_pattern "https://gitlab.com/fOO-Bar/w00den-baRK.ab.cd", user: "fOO-Bar", repo: "w00den-baRK.ab.cd" - assert_matches_pattern "https://gitlab.com/fOO-Bar/w00den-baRK.ab.cd.git", user: "fOO-Bar", repo: "w00den-baRK.ab.cd" - assert_matches_pattern "https://gitlab.com/foo_bar/_baz-buzz.cx", user: "foo_bar", repo: "_baz-buzz.cx" - end - end - describe ".anchor_link" do it "generates the correct anchor link" do Crystal::Doc.anchor_link("anchor").should eq( diff --git a/spec/compiler/crystal/tools/format_spec.cr b/spec/compiler/crystal/tools/format_spec.cr index b8a1a8c753a8..d0b37e866e29 100644 --- a/spec/compiler/crystal/tools/format_spec.cr +++ b/spec/compiler/crystal/tools/format_spec.cr @@ -99,8 +99,8 @@ describe Crystal::Command::FormatCommand do format_command = Crystal::Command::FormatCommand.new([] of String, color: false, stdin: stdin, stdout: stdout, stderr: stderr) format_command.run format_command.status_code.should eq(0) - stdout.to_s.should contain("Format ./format.cr") - stdout.to_s.should_not contain("Format ./not_format.cr") + stdout.to_s.should contain("Format #{Path[".", "format.cr"]}") + stdout.to_s.should_not contain("Format #{Path[".", "not_format.cr"]}") stderr.to_s.empty?.should be_true File.read(File.join(path, "format.cr")).should eq("if true\n 1\nend\n") @@ -124,8 +124,8 @@ describe Crystal::Command::FormatCommand do format_command = Crystal::Command::FormatCommand.new(["dir"], color: false, stdin: stdin, stdout: stdout, stderr: stderr) format_command.run format_command.status_code.should eq(0) - stdout.to_s.should contain("Format ./dir/format.cr") - stdout.to_s.should_not contain("Format ./dir/not_format.cr") + stdout.to_s.should contain("Format #{Path[".", "dir", "format.cr"]}") + stdout.to_s.should_not contain("Format #{Path[".", "dir", "not_format.cr"]}") stderr.to_s.empty?.should be_true {stdout, stderr}.each &.clear @@ -133,10 +133,10 @@ describe Crystal::Command::FormatCommand do format_command = Crystal::Command::FormatCommand.new([] of String, color: false, stdin: stdin, stdout: stdout, stderr: stderr) format_command.run format_command.status_code.should eq(0) - stdout.to_s.should contain("Format ./format.cr") - stdout.to_s.should_not contain("Format ./not_format.cr") - stdout.to_s.should_not contain("Format ./dir/format.cr") - stdout.to_s.should_not contain("Format ./dir/not_format.cr") + stdout.to_s.should contain("Format #{Path[".", "format.cr"]}") + stdout.to_s.should_not contain("Format #{Path[".", "not_format.cr"]}") + stdout.to_s.should_not contain("Format #{Path[".", "dir", "format.cr"]}") + stdout.to_s.should_not contain("Format #{Path[".", "dir", "not_format.cr"]}") stderr.to_s.empty?.should be_true File.read(File.join(path, "format.cr")).should eq("if true\n 1\nend\n") @@ -160,9 +160,9 @@ describe Crystal::Command::FormatCommand do format_command = Crystal::Command::FormatCommand.new([] of String, color: false, stdin: stdin, stdout: stdout, stderr: stderr) format_command.run format_command.status_code.should eq(1) - stdout.to_s.should contain("Format ./format.cr") - stderr.to_s.should contain("syntax error in './syntax_error.cr:1:3': unexpected token: EOF") - stderr.to_s.should contain("file './invalid_byte_sequence_error.cr' is not a valid Crystal source file: Unexpected byte 0xff at position 1, malformed UTF-8") + stdout.to_s.should contain("Format #{Path[".", "format.cr"]}") + stderr.to_s.should contain("syntax error in '#{Path[".", "syntax_error.cr"]}:1:3': unexpected token: EOF") + stderr.to_s.should contain("file '#{Path[".", "invalid_byte_sequence_error.cr"]}' is not a valid Crystal source file: Unexpected byte 0xff at position 1, malformed UTF-8") File.read(File.join(path, "format.cr")).should eq("if true\n 1\nend\n") end @@ -182,7 +182,7 @@ describe Crystal::Command::FormatCommand do format_command = BuggyFormatCommand.new([] of String, color: false, stdin: stdin, stdout: stdout, stderr: stderr) format_command.run format_command.status_code.should eq(1) - stderr.to_s.should contain("there's a bug formatting './empty.cr', to show more information, please run:\n\n $ crystal tool format --show-backtrace './empty.cr'") + stderr.to_s.should contain("there's a bug formatting '#{Path[".", "empty.cr"]}', to show more information, please run:\n\n $ crystal tool format --show-backtrace '#{Path[".", "empty.cr"]}'") end end end @@ -201,7 +201,7 @@ describe Crystal::Command::FormatCommand do format_command.run format_command.status_code.should eq(1) stderr.to_s.should contain("format command test") - stderr.to_s.should contain("couldn't format './empty.cr', please report a bug including the contents of it: https://github.com/crystal-lang/crystal/issues") + stderr.to_s.should contain("couldn't format '#{Path[".", "empty.cr"]}', please report a bug including the contents of it: https://github.com/crystal-lang/crystal/issues") end end end @@ -224,9 +224,9 @@ describe Crystal::Command::FormatCommand do format_command.status_code.should eq(1) stdout.to_s.empty?.should be_true stderr.to_s.should_not contain("not_format.cr") - stderr.to_s.should contain("formatting './format.cr' produced changes") - stderr.to_s.should contain("syntax error in './syntax_error.cr:1:3': unexpected token: EOF") - stderr.to_s.should contain("file './invalid_byte_sequence_error.cr' is not a valid Crystal source file: Unexpected byte 0xff at position 1, malformed UTF-8") + stderr.to_s.should contain("formatting '#{Path[".", "format.cr"]}' produced changes") + stderr.to_s.should contain("syntax error in '#{Path[".", "syntax_error.cr"]}:1:3': unexpected token: EOF") + stderr.to_s.should contain("file '#{Path[".", "invalid_byte_sequence_error.cr"]}' is not a valid Crystal source file: Unexpected byte 0xff at position 1, malformed UTF-8") end end end @@ -286,7 +286,7 @@ describe Crystal::Command::FormatCommand do format_command.run format_command.status_code.should eq(1) stdout.to_s.empty?.should be_true - stderr.to_s.should contain("formatting './format.cr' produced changes") + stderr.to_s.should contain("formatting '#{Path[".", "format.cr"]}' produced changes") end end end diff --git a/spec/compiler/crystal/tools/init_spec.cr b/spec/compiler/crystal/tools/init_spec.cr index c16409e35dd1..c82016d1a57d 100644 --- a/spec/compiler/crystal/tools/init_spec.cr +++ b/spec/compiler/crystal/tools/init_spec.cr @@ -6,6 +6,8 @@ require "ini" require "spec" require "yaml" require "../../../support/tempfile" +require "../../../support/env" +require "../../../support/win32" private def exec_init(project_name, project_dir = nil, type = "lib", force = false, skip_existing = false) args = [type, project_name] @@ -41,6 +43,24 @@ end module Crystal describe Init::InitProject do + it "correctly uses git config" do + within_temporary_directory do + File.write(".gitconfig", <<-CONTENT) + [user] + email = dorian@dorianmarie.fr + name = Dorian Marié + CONTENT + + with_env("GIT_CONFIG": "#{FileUtils.pwd}/.gitconfig") do + exec_init("example", "example", "app") + end + + with_file "example/LICENSE" do |file| + file.should contain("Dorian Marié") + end + end + end + it "has proper contents" do within_temporary_directory do run_init_project("lib", "example", "John Smith", "john@smith.com", "jsmith") @@ -280,7 +300,7 @@ module Crystal config.expanded_dir.should eq ::Path[Dir.current, "foo", "bar"] end - it "DIR (relative to home)" do + pending_win32 "DIR (relative to home)" do path = ::Path["~", "foo"].to_s config = Crystal::Init.parse_args(["lib", path]) config.name.should eq "foo" diff --git a/spec/compiler/crystal/tools/playground_spec.cr b/spec/compiler/crystal/tools/playground_spec.cr index 2e122ab8912c..4710f4fb0521 100644 --- a/spec/compiler/crystal/tools/playground_spec.cr +++ b/spec/compiler/crystal/tools/playground_spec.cr @@ -1,3 +1,5 @@ +{% skip_file if flag?(:without_playground) %} + require "../../../spec_helper" private def instrument(source) diff --git a/spec/compiler/crystal_path/crystal_path_spec.cr b/spec/compiler/crystal_path/crystal_path_spec.cr index 8cbe0ab8376d..390f91cd7c03 100644 --- a/spec/compiler/crystal_path/crystal_path_spec.cr +++ b/spec/compiler/crystal_path/crystal_path_spec.cr @@ -1,21 +1,26 @@ require "../../spec_helper" +require "../../support/env" private def assert_finds(search, results, relative_to = nil, path = __DIR__, file = __FILE__, line = __LINE__) it "finds #{search.inspect}", file, line do crystal_path = Crystal::CrystalPath.new(path) - relative_to = "#{__DIR__}/#{relative_to}" if relative_to - results = results.map { |result| "#{__DIR__}/#{result}" } - matches = crystal_path.find search, relative_to: relative_to - matches.should eq(results) + results = results.map { |result| ::Path[__DIR__, result].normalize.to_s } + Dir.cd(__DIR__) do + matches = crystal_path.find search, relative_to: relative_to + matches.should eq(results), file: file, line: line + end end end -private def assert_doesnt_find(search, relative_to = nil, path = __DIR__, file = __FILE__, line = __LINE__) +private def assert_doesnt_find(search, relative_to = nil, path = __DIR__, expected_relative_to = nil, file = __FILE__, line = __LINE__) it "doesn't finds #{search.inspect}", file, line do crystal_path = Crystal::CrystalPath.new(path) - relative_to = "#{__DIR__}/#{relative_to}" if relative_to - expect_raises Exception, /can't find file/ do - crystal_path.find search, relative_to: relative_to + Dir.cd(__DIR__) do + error = expect_raises Crystal::CrystalPath::NotFoundError do + crystal_path.find search, relative_to: relative_to + end + error.relative_to.should eq(expected_relative_to), file: file, line: line + error.filename.should eq(search), file: file, line: line end end end @@ -87,31 +92,29 @@ describe Crystal::CrystalPath do assert_doesnt_find "file_two.cr" assert_doesnt_find "test_folder/file_three.cr" - assert_doesnt_find "test_folder/*", relative_to: "#{__DIR__}/test_files/file_one.cr" + assert_doesnt_find "test_folder/*", relative_to: Path[__DIR__, "test_files", "file_one.cr"].to_s, expected_relative_to: Path[__DIR__, "test_files"].to_s assert_doesnt_find "test_files/missing_file.cr" assert_doesnt_find __FILE__[1..-1], path: ":" # Don't find in CRYSTAL_PATH if the path is relative (#4742) - assert_doesnt_find "./crystal_path_spec", relative_to: "test_files/file_one.cr" - assert_doesnt_find "./crystal_path_spec.cr", relative_to: "test_files/file_one.cr" + assert_doesnt_find "./crystal_path_spec", relative_to: Path["test_files", "file_one.cr"].to_s, expected_relative_to: Path["test_files"].to_s + assert_doesnt_find "./crystal_path_spec.cr", relative_to: Path["test_files", "file_one.cr"].to_s, expected_relative_to: Path["test_files"].to_s assert_doesnt_find "../crystal_path/test_files/file_one" # Don't find relative filenames in src or shards - assert_doesnt_find "../../src/file_three", relative_to: "test_files/test_folder/test_folder.cr" + assert_doesnt_find "../../src/file_three", relative_to: Path["test_files", "test_folder", "test_folder.cr"].to_s, expected_relative_to: Path["test_files", "test_folder"].to_s - it "prints an explanatory message for non-relative requires" do - crystal_path = Crystal::CrystalPath.new(__DIR__) - ex = expect_raises Exception, /If you're trying to require a shard/ do - crystal_path.find "non_existent", relative_to: __DIR__ + it "includes 'lib' by default" do + with_env("CRYSTAL_PATH": nil) do + crystal_path = Crystal::CrystalPath.new + crystal_path.entries[0].should eq("lib") end end - it "doesn't print an explanatory message for relative requires" do - crystal_path = Crystal::CrystalPath.new(__DIR__) - ex = expect_raises Exception, /can't find file/ do - crystal_path.find "./non_existent", relative_to: __DIR__ + it "overrides path with environment variable" do + with_env("CRYSTAL_PATH": "foo#{Process::PATH_DELIMITER}bar") do + crystal_path = Crystal::CrystalPath.new + crystal_path.entries.should eq(%w(foo bar)) end - - ex.message.not_nil!.should_not contain "If you're trying to require a shard" end end diff --git a/spec/compiler/data/args_test b/spec/compiler/data/args_test new file mode 100644 index 000000000000..86efc6f59fbb --- /dev/null +++ b/spec/compiler/data/args_test @@ -0,0 +1,7 @@ +# Last argument is file path to write +test_path = ARGV.pop +File.open(test_path, "w") do |f| + ARGV.inspect(f) + f.puts + {other_flag: {{flag?(:other_flag)}}}.inspect(f) +end diff --git a/spec/compiler/formatter/formatter_spec.cr b/spec/compiler/formatter/formatter_spec.cr index b37f01baac98..6705ae01ca3f 100644 --- a/spec/compiler/formatter/formatter_spec.cr +++ b/spec/compiler/formatter/formatter_spec.cr @@ -219,12 +219,12 @@ describe Crystal::Formatter do assert_format "def foo(a : T) forall T \n #\nend", "def foo(a : T) forall T\n #\nend" assert_format "def foo(a : T, b : U) forall T, U\n #\nend", "def foo(a : T, b : U) forall T, U\n #\nend" assert_format "def foo(a : T, b : U) forall T, U #\n #\nend", "def foo(a : T, b : U) forall T, U #\n #\nend" - assert_format "def foo(a : T) forall T\n #\n\nend", "def foo(a : T) forall T\n #\n\nend" - assert_format "def foo(a : T) forall T\n #\n\n\nend", "def foo(a : T) forall T\n #\n\nend" + assert_format "def foo(a : T) forall T\n #\n\nend", "def foo(a : T) forall T\n #\nend" + assert_format "def foo(a : T) forall T\n #\n\n\nend", "def foo(a : T) forall T\n #\nend" assert_format "def foo\n 1\n #\nrescue\nend" - assert_format "def foo\n 1 #\n\nrescue\nend" assert_format "def foo\n 1 #\nrescue\nend" - assert_format "def foo\n 1\n #\n\n\nrescue\nend", "def foo\n 1\n #\n\nrescue\nend" + assert_format "def foo\n 1 #\nrescue\nend" + assert_format "def foo\n 1\n #\n\n\nrescue\nend", "def foo\n 1\n #\nrescue\nend" assert_format "loop do\n 1\nrescue\n 2\nend" assert_format "loop do\n 1\n loop do\n 2\n rescue\n 3\n end\n 4\nend" @@ -277,6 +277,7 @@ describe Crystal::Formatter do assert_format "foo 1,\n2", "foo 1,\n 2" assert_format "foo 1, a: 1,\nb: 2,\nc: 3", "foo 1, a: 1,\n b: 2,\n c: 3" assert_format "foo 1,\na: 1,\nb: 2,\nc: 3", "foo 1,\n a: 1,\n b: 2,\n c: 3" + assert_format "foo bar:baz, qux:other", "foo bar: baz, qux: other" assert_format "foo(\n 1, 2, &block)", "foo(\n 1, 2, &block)" assert_format "foo(\n 1, 2,\n&block)", "foo(\n 1, 2,\n &block)" assert_format "foo(\n 1,\n 2\n) do\n 1\nend" @@ -542,6 +543,8 @@ describe Crystal::Formatter do assert_format "case\nend" assert_format "case\nelse\n 1\nend" + assert_format "case 1 \n in Int32 \n 3 \n end", "case 1\nin Int32\n 3\nend" + assert_format <<-CODE case 0 when 0 then 1; 2 @@ -720,6 +723,9 @@ describe Crystal::Formatter do assert_format "->( x , y ) { x }", "->(x, y) { x }" assert_format "->( x : Int32 , y ) { x }", "->(x : Int32, y) { x }" + assert_format "->@foo.foo" + assert_format "->@@foo.foo" + {:+, :-, :*, :/, :^, :>>, :<<, :|, :&, :&+, :&-, :&*, :&**}.each do |sym| assert_format ":#{sym}" end @@ -846,6 +852,7 @@ describe Crystal::Formatter do assert_format "if 1\n ((1) + 2)\nend" assert_format "def foo(x : self ?) \n end", "def foo(x : self?)\nend" + assert_format "def foo(x : (self)?)\nend" assert_format " macro foo\n end\n\n :+", "macro foo\n end\n\n:+" assert_format "[\n1, # a\n2, # b\n 3 # c\n]", "[\n 1, # a\n 2, # b\n 3, # c\n]" @@ -968,6 +975,12 @@ describe Crystal::Formatter do assert_format "<<-HTML\n \#{1}x\n HTML" assert_format "<<-HTML\n \#{1}x\n y\n HTML" assert_format "<<-HTML\n \#{1}x\n y\n z\n HTML" + assert_format %(<<-HTML\n \#{"foo"}\n HTML) + assert_format %(<<-HTML\n \#{__FILE__}\n HTML) + assert_format %(<<-HTML\n \#{"fo\#{"o"}"}\n HTML) + assert_format %(<<-HTML\n \#{"foo"}\#{1}\n HTML) + assert_format %(<<-HTML\n foo\n \#{"foo"}\n HTML) + assert_format %(<<-HTML\n \#{"foo"}\n \#{"bar"}\n HTML) assert_format " <<-HTML\n foo\n HTML", "<<-HTML\n foo\nHTML" assert_format " <<-HTML\n \#{1}\n HTML", "<<-HTML\n \#{1}\nHTML" @@ -987,6 +1000,8 @@ describe Crystal::Formatter do assert_format "<<-FOO\n#{"foo"}bar\nFOO" assert_format "<<-FOO\nbar#{"foo"}\nFOO" assert_format "<<-FOO\nbar#{"foo"}bar\nFOO" + assert_format "<<-FOO\nfoo\n#{"foo"}\nFOO" + assert_format "<<-FOO\nfoo\n#{1}\nFOO" assert_format "#!shebang\n1 + 2" @@ -1028,6 +1043,7 @@ describe Crystal::Formatter do assert_format "foo : self?" assert_format "foo : self? | A" + assert_format "foo : (self)?" assert_format "foo : (A) | D" assert_format "foo : (F(A)) | D" @@ -1039,6 +1055,7 @@ describe Crystal::Formatter do assert_format "module Readline\n @@completion_proc : (String -> Array(String)?) | (String -> Array(String)) | Nil\nend" assert_format "alias A = (B(C, (C | D)) | E)" assert_format "alias A = ((B(C | D) | E) | F)" + assert_format "alias A = ({A, (B)})" assert_format "foo : A(B)\nbar : C" assert_format "foo : (A -> B)\nbar : C" @@ -1602,6 +1619,9 @@ describe Crystal::Formatter do CODE assert_format "a.!" + assert_format "a &.!" + assert_format "a &.a.!" + assert_format "a &.!.!" assert_format <<-CODE ->{ @@ -1650,4 +1670,101 @@ describe Crystal::Formatter do 1 # foo / #{1} / CODE + + assert_format <<-BEFORE, + def foo + # Comment + + + end + BEFORE + <<-AFTER + def foo + # Comment + end + AFTER + + assert_format <<-BEFORE, + def foo + 1 + # Comment + + + end + BEFORE + <<-AFTER + def foo + 1 + # Comment + end + AFTER + + assert_format <<-CODE + def foo + 1 + end + + # Comment + + def bar + 2 + end + CODE + + assert_format <<-CODE + require "foo" + + @x : Int32 + + class Bar + end + CODE + + assert_format <<-CODE + x = <<-FOO + hello + FOO + + def bar + end + CODE + + assert_format <<-BEFORE, <<-AFTER + begin + 1 + # Comment + + + end + BEFORE + begin + 1 + # Comment + end + AFTER + + assert_format <<-BEFORE, <<-AFTER + begin + # Comment + + + end + BEFORE + begin + # Comment + end + AFTER + + assert_format <<-CODE + foo 1, # comment + do + end + CODE + + assert_format <<-CODE + foo 1, # comment + # bar + do + end + CODE end diff --git a/spec/compiler/lexer/lexer_macro_spec.cr b/spec/compiler/lexer/lexer_macro_spec.cr index a8eb7bf4255b..c58e1a177eea 100644 --- a/spec/compiler/lexer/lexer_macro_spec.cr +++ b/spec/compiler/lexer/lexer_macro_spec.cr @@ -39,7 +39,7 @@ describe "Lexer macro" do token.type.should eq(:MACRO_END) end - ["begin", "do", "if", "unless", "class", "struct", "module", "def", "while", "until", "case", "macro", "fun", "lib", "union", "annotation"].each do |keyword| + ["begin", "do", "if", "unless", "class", "struct", "module", "def", "while", "until", "case", "macro", "fun", "lib", "union", "annotation", "select"].each do |keyword| it "lexes macro with nested #{keyword}" do lexer = Lexer.new(%(hello\n #{keyword} {{world}} end end)) diff --git a/spec/compiler/macro/macro_methods_spec.cr b/spec/compiler/macro/macro_methods_spec.cr index b590b15341aa..0739ad312fe6 100644 --- a/spec/compiler/macro/macro_methods_spec.cr +++ b/spec/compiler/macro/macro_methods_spec.cr @@ -430,6 +430,10 @@ module Crystal assert_macro "", %({{"FooBar".underscore}}), [] of ASTNode, %("foo_bar") end + it "executes titleize" do + assert_macro "", %({{"hello world".titleize}}), [] of ASTNode, %("Hello World") + end + it "executes to_i" do assert_macro "", %({{"1234".to_i}}), [] of ASTNode, %(1234) end @@ -477,6 +481,7 @@ module Crystal assert_macro "x", %({{x.starts_with?("hel")}}), [MacroId.new("hello")] of ASTNode, %(true) assert_macro "x", %({{x.chomp}}), [MacroId.new("hello\n")] of ASTNode, %(hello) assert_macro "x", %({{x.upcase}}), [MacroId.new("hello")] of ASTNode, %(HELLO) + assert_macro "x", %({{x.titleize}}), [MacroId.new("hello world")] of ASTNode, %(Hello World) assert_macro "x", %({{x.includes?("el")}}), [MacroId.new("hello")] of ASTNode, %(true) assert_macro "x", %({{x.includes?("he")}}), [MacroId.new("hello")] of ASTNode, %(true) assert_macro "x", %({{x.includes?("EL")}}), [MacroId.new("hello")] of ASTNode, %(false) @@ -519,6 +524,7 @@ module Crystal assert_macro "x", %({{x.starts_with?("hel")}}), ["hello".symbol] of ASTNode, %(true) assert_macro "x", %({{x.chomp}}), [SymbolLiteral.new("hello\n")] of ASTNode, %(:hello) assert_macro "x", %({{x.upcase}}), ["hello".symbol] of ASTNode, %(:HELLO) + assert_macro "x", %({{x.titleize}}), ["hello world".symbol] of ASTNode, %(:"Hello World") assert_macro "x", %({{x.includes?("el")}}), ["hello".symbol] of ASTNode, %(true) assert_macro "x", %({{x.includes?("he")}}), ["hello".symbol] of ASTNode, %(true) assert_macro "x", %({{x.includes?("EL")}}), ["hello".symbol] of ASTNode, %(false) @@ -1423,6 +1429,10 @@ module Crystal [TypeNode.new(GenericClassType.new(program, program, "SomeType", program.object, ["A", "B"]))] of ASTNode end end + + it "includes the generic_args of the instantiated type by default" do + assert_macro("", "{{Array(Int32).name}}", [] of ASTNode, "Array(Int32)") + end end describe :generic_args do @@ -1432,6 +1442,10 @@ module Crystal [TypeNode.new(GenericClassType.new(program, program, "SomeType", program.object, ["A", "B"]))] of ASTNode end end + + it "includes the generic_args of the instantiated type" do + assert_macro("", "{{Array(Int32).name(generic_args: true)}}", [] of ASTNode, "Array(Int32)") + end end describe false do @@ -1440,6 +1454,10 @@ module Crystal [TypeNode.new(GenericClassType.new(program, program, "SomeType", program.object, ["A", "B"]))] of ASTNode end end + + it "does not include the generic_args of the instantiated type" do + assert_macro("", "{{Array(Int32).name(generic_args: false)}}", [] of ASTNode, "Array") + end end describe "with an invalid type argument" do @@ -1640,15 +1658,300 @@ module Crystal end end - it "executes nilable? (false)" do - assert_macro("x", "{{x.nilable?}}", "false") do |program| - [TypeNode.new(program.string)] of ASTNode + describe "#abstract?" do + it NonGenericModuleType do + assert_macro("type", "{{type.abstract?}}", "false") do |program| + mod = NonGenericModuleType.new(program, program, "SomeModule") + + [TypeNode.new(mod)] of ASTNode + end + end + + it GenericModuleType do + assert_macro("type", "{{type.abstract?}}", "false") do |program| + generic_mod = GenericModuleType.new(program, program, "SomeGenericModule", ["T"]) + + [TypeNode.new(generic_mod)] of ASTNode + end + end + + describe NonGenericClassType do + describe "class" do + it "abstract" do + assert_macro("type", "{{type.abstract?}}", "true") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + klass.abstract = true + + [TypeNode.new(klass)] of ASTNode + end + end + + it "non-abstract" do + assert_macro("type", "{{type.abstract?}}", "false") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + + [TypeNode.new(klass)] of ASTNode + end + end + end + + describe "struct" do + it "abstract" do + assert_macro("type", "{{type.abstract?}}", "true") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + klass.abstract = true + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + + it "non-abstract" do + assert_macro("type", "{{type.abstract?}}", "false") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + end + + describe GenericClassType do + describe "class" do + it "abstract" do + assert_macro("type", "{{type.abstract?}}", "true") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + klass.abstract = true + + [TypeNode.new(klass)] of ASTNode + end + end + + it "non-abstract" do + assert_macro("type", "{{type.abstract?}}", "false") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + + [TypeNode.new(klass)] of ASTNode + end + end + end + + describe "struct" do + it "abstract" do + assert_macro("type", "{{type.abstract?}}", "true") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + klass.abstract = true + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + + it "non-abstract" do + assert_macro("type", "{{type.abstract?}}", "false") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end end end - it "executes nilable? (true)" do - assert_macro("x", "{{x.nilable?}}", "true") do |program| - [TypeNode.new(program.union_of(program.string, program.nil))] of ASTNode + describe "#union?" do + it true do + assert_macro("x", "{{x.union?}}", "true") do |program| + [TypeNode.new(program.union_of(program.string, program.nil))] of ASTNode + end + end + + it false do + assert_macro("x", "{{x.union?}}", "false") do |program| + [TypeNode.new(program.string)] of ASTNode + end + end + end + + describe "#module?" do + it NonGenericModuleType do + assert_macro("type", "{{type.module?}}", "true") do |program| + mod = NonGenericModuleType.new(program, program, "SomeModule") + + [TypeNode.new(mod)] of ASTNode + end + end + + it GenericModuleType do + assert_macro("type", "{{type.module?}}", "true") do |program| + generic_mod = GenericModuleType.new(program, program, "SomeGenericModule", ["T"]) + + [TypeNode.new(generic_mod)] of ASTNode + end + end + + describe NonGenericClassType do + it "class" do + assert_macro("type", "{{type.module?}}", "false") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + + [TypeNode.new(klass)] of ASTNode + end + end + + it "struct" do + assert_macro("type", "{{type.module?}}", "false") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + + describe GenericClassType do + it "class" do + assert_macro("type", "{{type.module?}}", "false") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + + [TypeNode.new(klass)] of ASTNode + end + end + + it "struct" do + assert_macro("type", "{{type.module?}}", "false") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + end + + describe "#class?" do + it NonGenericModuleType do + assert_macro("type", "{{type.class?}}", "false") do |program| + mod = NonGenericModuleType.new(program, program, "SomeModule") + + [TypeNode.new(mod)] of ASTNode + end + end + + it GenericModuleType do + assert_macro("type", "{{type.class?}}", "false") do |program| + generic_mod = GenericModuleType.new(program, program, "SomeGenericModule", ["T"]) + + [TypeNode.new(generic_mod)] of ASTNode + end + end + + describe NonGenericClassType do + it "class" do + assert_macro("type", "{{type.class?}}", "true") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + + [TypeNode.new(klass)] of ASTNode + end + end + + it "struct" do + assert_macro("type", "{{type.class?}}", "false") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + + describe GenericClassType do + it "class" do + assert_macro("type", "{{type.class?}}", "true") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + + [TypeNode.new(klass)] of ASTNode + end + end + + it "struct" do + assert_macro("type", "{{type.class?}}", "false") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + end + + describe "#struct?" do + it NonGenericModuleType do + assert_macro("type", "{{type.struct?}}", "false") do |program| + mod = NonGenericModuleType.new(program, program, "SomeModule") + + [TypeNode.new(mod)] of ASTNode + end + end + + it GenericModuleType do + assert_macro("type", "{{type.struct?}}", "false") do |program| + generic_mod = GenericModuleType.new(program, program, "SomeGenericModule", ["T"]) + + [TypeNode.new(generic_mod)] of ASTNode + end + end + + describe NonGenericClassType do + it "class" do + assert_macro("type", "{{type.struct?}}", "false") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + + [TypeNode.new(klass)] of ASTNode + end + end + + it "struct" do + assert_macro("type", "{{type.struct?}}", "true") do |program| + klass = NonGenericClassType.new(program, program, "SomeType", program.reference) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + + describe GenericClassType do + it "class" do + assert_macro("type", "{{type.struct?}}", "false") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + + [TypeNode.new(klass)] of ASTNode + end + end + + it "struct" do + assert_macro("type", "{{type.struct?}}", "true") do |program| + klass = GenericClassType.new(program, program, "SomeGenericType", program.reference, ["T"]) + klass.struct = true + + [TypeNode.new(klass)] of ASTNode + end + end + end + end + describe "#nilable?" do + it false do + assert_macro("x", "{{x.nilable?}}", "false") do |program| + [TypeNode.new(program.string)] of ASTNode + end + end + + it true do + assert_macro("x", "{{x.nilable?}}", "true") do |program| + [TypeNode.new(program.union_of(program.string, program.nil))] of ASTNode + end end end @@ -1978,26 +2281,36 @@ module Crystal end describe "case methods" do - case_node = Case.new(1.int32, [When.new([2.int32, 3.int32] of ASTNode, 4.int32)], 5.int32) + describe "when" do + case_node = Case.new(1.int32, [When.new([2.int32, 3.int32] of ASTNode, 4.int32)], 5.int32, exhaustive: false) - it "executes cond" do - assert_macro "x", %({{x.cond}}), [case_node] of ASTNode, "1" - end + it "executes cond" do + assert_macro "x", %({{x.cond}}), [case_node] of ASTNode, "1" + end - it "executes whens" do - assert_macro "x", %({{x.whens}}), [case_node] of ASTNode, "[when 2, 3\n 4\n]" - end + it "executes whens" do + assert_macro "x", %({{x.whens}}), [case_node] of ASTNode, "[when 2, 3\n 4\n]" + end - it "executes when conds" do - assert_macro "x", %({{x.whens[0].conds}}), [case_node] of ASTNode, "[2, 3]" - end + it "executes when conds" do + assert_macro "x", %({{x.whens[0].conds}}), [case_node] of ASTNode, "[2, 3]" + end + + it "executes when body" do + assert_macro "x", %({{x.whens[0].body}}), [case_node] of ASTNode, "4" + end - it "executes when body" do - assert_macro "x", %({{x.whens[0].body}}), [case_node] of ASTNode, "4" + it "executes else" do + assert_macro "x", %({{x.else}}), [case_node] of ASTNode, "5" + end end - it "executes else" do - assert_macro "x", %({{x.else}}), [case_node] of ASTNode, "5" + describe "in" do + case_node = Case.new(1.int32, [When.new([2.int32, 3.int32] of ASTNode, 4.int32)], 5.int32, exhaustive: true) + + it "executes whens" do + assert_macro "x", %({{x.whens}}), [case_node] of ASTNode, "[in 2, 3\n 4\n]" + end end end diff --git a/spec/compiler/normalize/case_spec.cr b/spec/compiler/normalize/case_spec.cr index 6d56aeefbdcd..5ad7da9a61b0 100644 --- a/spec/compiler/normalize/case_spec.cr +++ b/spec/compiler/normalize/case_spec.cr @@ -6,103 +6,103 @@ describe "Normalize: case" do end it "normalizes case with var in cond" do - assert_expand_second "x = 1; case x; when 1; 'b'; else; end", "if 1 === x\n 'b'\nend" + assert_expand_second "x = 1; case x; when 1; 'b'; end", "if 1 === x\n 'b'\nend" end it "normalizes case with Path to is_a?" do - assert_expand_second "x = 1; case x; when Foo; 'b'; else; end", "if x.is_a?(Foo)\n 'b'\nend" + assert_expand_second "x = 1; case x; when Foo; 'b'; end", "if x.is_a?(Foo)\n 'b'\nend" end it "normalizes case with generic to is_a?" do - assert_expand_second "x = 1; case x; when Foo(T); 'b'; else; end", "if x.is_a?(Foo(T))\n 'b'\nend" + assert_expand_second "x = 1; case x; when Foo(T); 'b'; end", "if x.is_a?(Foo(T))\n 'b'\nend" end it "normalizes case with Path.class to is_a?" do - assert_expand_second "x = 1; case x; when Foo.class; 'b'; else; end", "if x.is_a?(Foo.class)\n 'b'\nend" + assert_expand_second "x = 1; case x; when Foo.class; 'b'; end", "if x.is_a?(Foo.class)\n 'b'\nend" end it "normalizes case with Generic.class to is_a?" do - assert_expand_second "x = 1; case x; when Foo(T).class; 'b'; else; end", "if x.is_a?(Foo(T).class)\n 'b'\nend" + assert_expand_second "x = 1; case x; when Foo(T).class; 'b'; end", "if x.is_a?(Foo(T).class)\n 'b'\nend" end it "normalizes case with many expressions in when" do - assert_expand_second "x = 1; case x; when 1, 2; 'b'; else; end", "if (1 === x) || (2 === x)\n 'b'\nend" + assert_expand_second "x = 1; case x; when 1, 2; 'b'; end", "if (1 === x) || (2 === x)\n 'b'\nend" end it "normalizes case with implicit call" do - assert_expand "case x; when .foo(1); 2; else; end", "__temp_1 = x\nif __temp_1.foo(1)\n 2\nend" + assert_expand "case x; when .foo(1); 2; end", "__temp_1 = x\nif __temp_1.foo(1)\n 2\nend" end it "normalizes case with implicit responds_to? (#3040)" do - assert_expand "case x; when .responds_to?(:foo); 2; else; end", "__temp_1 = x\nif __temp_1.responds_to?(:foo)\n 2\nend" + assert_expand "case x; when .responds_to?(:foo); 2; end", "__temp_1 = x\nif __temp_1.responds_to?(:foo)\n 2\nend" end it "normalizes case with implicit is_a? (#3040)" do - assert_expand "case x; when .is_a?(T); 2; else; end", "__temp_1 = x\nif __temp_1.is_a?(T)\n 2\nend" + assert_expand "case x; when .is_a?(T); 2; end", "__temp_1 = x\nif __temp_1.is_a?(T)\n 2\nend" end it "normalizes case with implicit as (#3040)" do - assert_expand "case x; when .as(T); 2; else; end", "__temp_1 = x\nif __temp_1.as(T)\n 2\nend" + assert_expand "case x; when .as(T); 2; end", "__temp_1 = x\nif __temp_1.as(T)\n 2\nend" end it "normalizes case with implicit as? (#3040)" do - assert_expand "case x; when .as?(T); 2; else; end", "__temp_1 = x\nif __temp_1.as?(T)\n 2\nend" + assert_expand "case x; when .as?(T); 2; end", "__temp_1 = x\nif __temp_1.as?(T)\n 2\nend" end it "normalizes case with implicit !" do - assert_expand "case x; when .!; 2; else; end", "__temp_1 = x\nif !__temp_1\n 2\nend" + assert_expand "case x; when .!; 2; end", "__temp_1 = x\nif !__temp_1\n 2\nend" end it "normalizes case with assignment" do - assert_expand "case x = 1; when 2; 3; else; end", "x = 1\nif 2 === x\n 3\nend" + assert_expand "case x = 1; when 2; 3; end", "x = 1\nif 2 === x\n 3\nend" end it "normalizes case with assignment wrapped by paren" do - assert_expand "case (x = 1); when 2; 3; else; end", "x = 1\nif 2 === x\n 3\nend" + assert_expand "case (x = 1); when 2; 3; end", "x = 1\nif 2 === x\n 3\nend" end it "normalizes case without value" do - assert_expand "case when 2; 3; when 4; 5; else; end", "if 2\n 3\nelse\n if 4\n 5\n end\nend" + assert_expand "case when 2; 3; when 4; 5; end", "if 2\n 3\nelse\n if 4\n 5\n end\nend" end it "normalizes case without value with many expressions in when" do - assert_expand "case when 2, 9; 3; when 4; 5; else; end", "if 2 || 9\n 3\nelse\n if 4\n 5\n end\nend" + assert_expand "case when 2, 9; 3; when 4; 5; end", "if 2 || 9\n 3\nelse\n if 4\n 5\n end\nend" end it "normalizes case with nil to is_a?" do - assert_expand_second "x = 1; case x; when nil; 'b'; else; end", "if x.is_a?(::Nil)\n 'b'\nend" + assert_expand_second "x = 1; case x; when nil; 'b'; end", "if x.is_a?(::Nil)\n 'b'\nend" end it "normalizes case with multiple expressions" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {2, 3}; 4; else; end", "if (2 === x) && (3 === y)\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {2, 3}; 4; end", "if (2 === x) && (3 === y)\n 4\nend" end it "normalizes case with multiple expressions and types" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {Int32, Float64}; 4; else; end", "if x.is_a?(Int32) && y.is_a?(Float64)\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {Int32, Float64}; 4; end", "if x.is_a?(Int32) && y.is_a?(Float64)\n 4\nend" end it "normalizes case with multiple expressions and implicit obj" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {.foo, .bar}; 4; else; end", "if x.foo && y.bar\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {.foo, .bar}; 4; end", "if x.foo && y.bar\n 4\nend" end it "normalizes case with multiple expressions and comma" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {2, 3}, {4, 5}; 6; else; end", "if ((2 === x) && (3 === y)) || ((4 === x) && (5 === y))\n 6\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {2, 3}, {4, 5}; 6; end", "if ((2 === x) && (3 === y)) || ((4 === x) && (5 === y))\n 6\nend" end it "normalizes case with multiple expressions with underscore" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {2, _}; 4; else; end", "if 2 === x\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {2, _}; 4; end", "if 2 === x\n 4\nend" end it "normalizes case with multiple expressions with all underscores" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {_, _}; 4; else; end", "if true\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {_, _}; 4; end", "if true\n 4\nend" end it "normalizes case with multiple expressions with all underscores twice" do - assert_expand_second "x, y = 1, 2; case {x, y}; when {_, _}, {_, _}; 4; else; end", "if true\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when {_, _}, {_, _}; 4; end", "if true\n 4\nend" end it "normalizes case with multiple expressions and non-tuple" do - assert_expand_second "x, y = 1, 2; case {x, y}; when 1; 4; else; end", "if 1 === {x, y}\n 4\nend" + assert_expand_second "x, y = 1, 2; case {x, y}; when 1; 4; end", "if 1 === {x, y}\n 4\nend" end it "normalizes case without when and else" do @@ -120,4 +120,8 @@ describe "Normalize: case" do it "normalizes case without cond, when but else" do assert_expand "case; else; y; end", "y" end + + it "normalizes case with Path.class to is_a? (in)" do + assert_expand_second "x = 1; case x; in Foo.class; 'b'; end", "if x.is_a?(Foo.class)\n 'b'\nelse\n raise \"unreachable\"\nend" + end end diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr index eef540a69702..cafa5246704a 100644 --- a/spec/compiler/parser/parser_spec.cr +++ b/spec/compiler/parser/parser_spec.cr @@ -111,6 +111,18 @@ module Crystal it_parses "1 / -2", Call.new(1.int32, "/", -2.int32) it_parses "2 / 3 + 4 / 5", Call.new(Call.new(2.int32, "/", 3.int32), "+", Call.new(4.int32, "/", 5.int32)) it_parses "2 * (3 + 4)", Call.new(2.int32, "*", Expressions.new([Call.new(3.int32, "+", 4.int32)] of ASTNode)) + it_parses "a = 1; b = 2; c = 3; a-b-c", Expressions.new([ + Assign.new("a".var, 1.int32), + Assign.new("b".var, 2.int32), + Assign.new("c".var, 3.int32), + Call.new(Call.new("a".var, "-", "b".var), "-", "c".var), + ]) + it_parses "a = 1; b = 2; c = 3; a-b -c", Expressions.new([ + Assign.new("a".var, 1.int32), + Assign.new("b".var, 2.int32), + Assign.new("c".var, 3.int32), + Call.new(Call.new("a".var, "-", "b".var), "-", "c".var), + ]) it_parses "1/2", Call.new(1.int32, "/", [2.int32] of ASTNode) it_parses "1 + /foo/", Call.new(1.int32, "+", regex("foo")) it_parses "1+0", Call.new(1.int32, "+", 0.int32) @@ -124,6 +136,7 @@ module Crystal it_parses "foo[] /2", Call.new(Call.new("foo".call, "[]"), "/", 2.int32) it_parses "foo[1] /2", Call.new(Call.new("foo".call, "[]", 1.int32), "/", 2.int32) it_parses "[1] /2", Call.new(([1.int32] of ASTNode).array, "/", 2.int32) + it_parses "2**3**4", Call.new(2.int32, "**", Call.new(3.int32, "**", 4.int32)) it_parses "!1", Not.new(1.int32) it_parses "- 1", Call.new(1.int32, "-") @@ -197,13 +210,16 @@ module Crystal extend class struct module enum while until return next break lib fun alias pointerof sizeof instance_sizeof offsetof typeof private protected asm out - end + end self in ).each do |kw| assert_syntax_error "def foo(#{kw}); end", "cannot use '#{kw}' as an argument name", 1, 9 assert_syntax_error "def foo(foo #{kw}); end", "cannot use '#{kw}' as an argument name", 1, 13 it_parses "def foo(#{kw} foo); end", Def.new("foo", [Arg.new("foo", external_name: kw.to_s)]) it_parses "def foo(@#{kw}); end", Def.new("foo", [Arg.new("__arg0", external_name: kw.to_s)], [Assign.new("@#{kw}".instance_var, "__arg0".var)] of ASTNode) it_parses "def foo(@@#{kw}); end", Def.new("foo", [Arg.new("__arg0", external_name: kw.to_s)], [Assign.new("@@#{kw}".class_var, "__arg0".var)] of ASTNode) + + assert_syntax_error "foo { |#{kw})| }", "cannot use '#{kw}' as a block argument name", 1, 8 + assert_syntax_error "foo { |(#{kw}))| }", "cannot use '#{kw}' as a block argument name", 1, 9 end it_parses "def self.foo\n1\nend", Def.new("foo", body: 1.int32, receiver: "self".var) @@ -412,6 +428,8 @@ module Crystal it_parses %(foo("foo bar": 1, "baz": 2)), Call.new(nil, "foo", named_args: [NamedArgument.new("foo bar", 1.int32), NamedArgument.new("baz", 2.int32)]) it_parses %(foo "foo bar": 1, "baz": 2), Call.new(nil, "foo", named_args: [NamedArgument.new("foo bar", 1.int32), NamedArgument.new("baz", 2.int32)]) + it_parses %(foo(Foo: 1, Bar: 2)), Call.new(nil, "foo", named_args: [NamedArgument.new("Foo", 1.int32), NamedArgument.new("Bar", 2.int32)]) + it_parses "x.foo(a: 1, b: 2)", Call.new("x".call, "foo", named_args: [NamedArgument.new("a", 1.int32), NamedArgument.new("b", 2.int32)]) it_parses "x.foo a: 1, b: 2 ", Call.new("x".call, "foo", named_args: [NamedArgument.new("a", 1.int32), NamedArgument.new("b", 2.int32)]) @@ -452,11 +470,11 @@ module Crystal ["/", "<", "<=", "==", "!=", "=~", "!~", ">", ">=", "+", "-", "*", "/", "~", "%", "&", "|", "^", "**", "==="].each do |op| it_parses "def #{op}; end;", Def.new(op) + it_parses "def #{op}(); end;", Def.new(op) + it_parses "def self.#{op}; end;", Def.new(op, receiver: "self".var) + it_parses "def self.#{op}(); end;", Def.new(op, receiver: "self".var) end - it_parses "def %(); end;", Def.new("%") - it_parses "def /(); end;", Def.new("/") - ["<<", "<", "<=", "==", ">>", ">", ">=", "+", "-", "*", "/", "//", "%", "|", "&", "^", "**", "===", "=~", "!~", "&+", "&-", "&*", "&**"].each do |op| it_parses "1 #{op} 2", Call.new(1.int32, op, 2.int32) it_parses "n #{op} 2", Call.new("n".call, op, 2.int32) @@ -552,6 +570,7 @@ module Crystal it_parses "Foo(x: U)", Generic.new("Foo".path, [] of ASTNode, named_args: [NamedArgument.new("x", "U".path)]) it_parses "Foo(x: U, y: V)", Generic.new("Foo".path, [] of ASTNode, named_args: [NamedArgument.new("x", "U".path), NamedArgument.new("y", "V".path)]) + it_parses "Foo(X: U, Y: V)", Generic.new("Foo".path, [] of ASTNode, named_args: [NamedArgument.new("X", "U".path), NamedArgument.new("Y", "V".path)]) assert_syntax_error "Foo(T, x: U)" assert_syntax_error "Foo(x: T y: U)" @@ -560,7 +579,9 @@ module Crystal it_parses "Foo({x: X})", Generic.new("Foo".path, [Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: [NamedArgument.new("x", "X".path)])] of ASTNode) it_parses "Foo({x: X, y: Y})", Generic.new("Foo".path, [Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: [NamedArgument.new("x", "X".path), NamedArgument.new("y", "Y".path)])] of ASTNode) + it_parses "Foo({X: X, Y: Y})", Generic.new("Foo".path, [Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: [NamedArgument.new("X", "X".path), NamedArgument.new("Y", "Y".path)])] of ASTNode) it_parses "Foo(T, {x: X})", Generic.new("Foo".path, ["T".path, Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: [NamedArgument.new("x", "X".path)])] of ASTNode) + it_parses "Foo({x: X, typeof: Y})", Generic.new("Foo".path, [Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: [NamedArgument.new("x", "X".path), NamedArgument.new("typeof", "Y".path)])] of ASTNode) assert_syntax_error "Foo({x: X, x: Y})", "duplicated key: x" it_parses %(Foo({"foo bar": X})), Generic.new("Foo".path, [Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: [NamedArgument.new("foo bar", "X".path)])] of ASTNode) @@ -1062,45 +1083,69 @@ module Crystal assert_syntax_error "{a: 1, a: 2}", "duplicated key: a" it_parses "{} of Int => Double", HashLiteral.new([] of HashLiteral::Entry, of: HashLiteral::Entry.new("Int".path, "Double".path)) + it_parses "{} of Int32 -> Int32 => Int32", HashLiteral.new([] of HashLiteral::Entry, of: HashLiteral::Entry.new(ProcNotation.new(["Int32".path] of ASTNode, "Int32".path), "Int32".path)) it_parses "require \"foo\"", Require.new("foo") it_parses "require \"foo\"; [1]", [Require.new("foo"), ([1.int32] of ASTNode).array] - it_parses "case 1; when 1; 2; else; 3; end", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32) - it_parses "case 1; when 0, 1; 2; else; 3; end", Case.new(1.int32, [When.new([0.int32, 1.int32] of ASTNode, 2.int32)], 3.int32) - it_parses "case 1\nwhen 1\n2\nelse\n3\nend", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32) - it_parses "case 1\nwhen 1\n2\nend", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)]) - it_parses "case / /; when / /; / /; else; / /; end", Case.new(regex(" "), [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" ")) - it_parses "case / /\nwhen / /\n/ /\nelse\n/ /\nend", Case.new(regex(" "), [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" ")) - - it_parses "case 1; when 1 then 2; else; 3; end", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32) - it_parses "case 1; when x then 2; else; 3; end", Case.new(1.int32, [When.new(["x".call] of ASTNode, 2.int32)], 3.int32) - it_parses "case 1\nwhen 1\n2\nend\nif a\nend", [Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)]), If.new("a".call)] - it_parses "case\n1\nwhen 1\n2\nend\nif a\nend", [Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)]), If.new("a".call)] - - it_parses "case 1\nwhen .foo\n2\nend", Case.new(1.int32, [When.new([Call.new(ImplicitObj.new, "foo")] of ASTNode, 2.int32)]) - it_parses "case 1\nwhen .responds_to?(:foo)\n2\nend", Case.new(1.int32, [When.new([RespondsTo.new(ImplicitObj.new, "foo")] of ASTNode, 2.int32)]) - it_parses "case 1\nwhen .is_a?(T)\n2\nend", Case.new(1.int32, [When.new([IsA.new(ImplicitObj.new, "T".path)] of ASTNode, 2.int32)]) - it_parses "case 1\nwhen .as(T)\n2\nend", Case.new(1.int32, [When.new([Cast.new(ImplicitObj.new, "T".path)] of ASTNode, 2.int32)]) - it_parses "case 1\nwhen .as?(T)\n2\nend", Case.new(1.int32, [When.new([NilableCast.new(ImplicitObj.new, "T".path)] of ASTNode, 2.int32)]) - it_parses "case 1\nwhen .!()\n2\nend", Case.new(1.int32, [When.new([Not.new(ImplicitObj.new)] of ASTNode, 2.int32)]) - it_parses "case when 1\n2\nend", Case.new(nil, [When.new([1.int32] of ASTNode, 2.int32)]) - it_parses "case \nwhen 1\n2\nend", Case.new(nil, [When.new([1.int32] of ASTNode, 2.int32)]) - it_parses "case {1, 2}\nwhen {3, 4}\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([3.int32, 4.int32] of ASTNode)] of ASTNode, 5.int32)]) - it_parses "case {1, 2}\nwhen {3, 4}, {5, 6}\n7\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([3.int32, 4.int32] of ASTNode), TupleLiteral.new([5.int32, 6.int32] of ASTNode)] of ASTNode, 7.int32)]) - it_parses "case {1, 2}\nwhen {.foo, .bar}\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([Call.new(ImplicitObj.new, "foo"), Call.new(ImplicitObj.new, "bar")] of ASTNode)] of ASTNode, 5.int32)]) - it_parses "case {1, 2}\nwhen foo\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new(["foo".call] of ASTNode, 5.int32)]) - it_parses "case a\nwhen b\n1 / 2\nelse\n1 / 2\nend", Case.new("a".call, [When.new(["b".call] of ASTNode, Call.new(1.int32, "/", 2.int32))], Call.new(1.int32, "/", 2.int32)) - it_parses "case a\nwhen b\n/ /\n\nelse\n/ /\nend", Case.new("a".call, [When.new(["b".call] of ASTNode, RegexLiteral.new(StringLiteral.new(" ")))], RegexLiteral.new(StringLiteral.new(" "))) + it_parses "case 1; when 1; 2; else; 3; end", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32, exhaustive: false) + it_parses "case 1; when 0, 1; 2; else; 3; end", Case.new(1.int32, [When.new([0.int32, 1.int32] of ASTNode, 2.int32)], 3.int32, exhaustive: false) + it_parses "case 1\nwhen 1\n2\nelse\n3\nend", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32, exhaustive: false) + it_parses "case 1\nwhen 1\n2\nend", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case / /; when / /; / /; else; / /; end", Case.new(regex(" "), [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" "), exhaustive: false) + it_parses "case / /\nwhen / /\n/ /\nelse\n/ /\nend", Case.new(regex(" "), [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" "), exhaustive: false) + + it_parses "case 1; when 1 then 2; else; 3; end", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32, exhaustive: false) + it_parses "case 1; when x then 2; else; 3; end", Case.new(1.int32, [When.new(["x".call] of ASTNode, 2.int32)], 3.int32, exhaustive: false) + it_parses "case 1\nwhen 1\n2\nend\nif a\nend", [Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], else: nil, exhaustive: false), If.new("a".call)] + it_parses "case\n1\nwhen 1\n2\nend\nif a\nend", [Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], else: nil, exhaustive: false), If.new("a".call)] + + it_parses "case 1\nwhen .foo\n2\nend", Case.new(1.int32, [When.new([Call.new(ImplicitObj.new, "foo")] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case 1\nwhen .responds_to?(:foo)\n2\nend", Case.new(1.int32, [When.new([RespondsTo.new(ImplicitObj.new, "foo")] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case 1\nwhen .is_a?(T)\n2\nend", Case.new(1.int32, [When.new([IsA.new(ImplicitObj.new, "T".path)] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case 1\nwhen .as(T)\n2\nend", Case.new(1.int32, [When.new([Cast.new(ImplicitObj.new, "T".path)] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case 1\nwhen .as?(T)\n2\nend", Case.new(1.int32, [When.new([NilableCast.new(ImplicitObj.new, "T".path)] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case 1\nwhen .!()\n2\nend", Case.new(1.int32, [When.new([Not.new(ImplicitObj.new)] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case when 1\n2\nend", Case.new(nil, [When.new([1.int32] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case \nwhen 1\n2\nend", Case.new(nil, [When.new([1.int32] of ASTNode, 2.int32)], else: nil, exhaustive: false) + it_parses "case {1, 2}\nwhen {3, 4}\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([3.int32, 4.int32] of ASTNode)] of ASTNode, 5.int32)], else: nil, exhaustive: false) + it_parses "case {1, 2}\nwhen {3, 4}, {5, 6}\n7\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([3.int32, 4.int32] of ASTNode), TupleLiteral.new([5.int32, 6.int32] of ASTNode)] of ASTNode, 7.int32)], else: nil, exhaustive: false) + it_parses "case {1, 2}\nwhen {.foo, .bar}\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([Call.new(ImplicitObj.new, "foo"), Call.new(ImplicitObj.new, "bar")] of ASTNode)] of ASTNode, 5.int32)], else: nil, exhaustive: false) + it_parses "case {1, 2}\nwhen foo\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new(["foo".call] of ASTNode, 5.int32)], else: nil, exhaustive: false) + it_parses "case a\nwhen b\n1 / 2\nelse\n1 / 2\nend", Case.new("a".call, [When.new(["b".call] of ASTNode, Call.new(1.int32, "/", 2.int32))], Call.new(1.int32, "/", 2.int32), exhaustive: false) + it_parses "case a\nwhen b\n/ /\n\nelse\n/ /\nend", Case.new("a".call, [When.new(["b".call] of ASTNode, RegexLiteral.new(StringLiteral.new(" ")))], RegexLiteral.new(StringLiteral.new(" ")), exhaustive: false) assert_syntax_error "case {1, 2}; when {3}; 4; end", "wrong number of tuple elements (given 1, expected 2)", 1, 19 - it_parses "case 1; end", [Case.new(1.int32, [] of When)] - it_parses "case foo; end", [Case.new("foo".call, [] of When)] - it_parses "case\nend", [Case.new(nil, [] of When)] - it_parses "case;end", [Case.new(nil, [] of When)] - it_parses "case 1\nelse\n2\nend", [Case.new(1.int32, [] of When, 2.int32)] - it_parses "a = 1\ncase 1\nwhen a then 1\nend", [Assign.new("a".var, 1.int32), Case.new(1.int32, [When.new(["a".var] of ASTNode, 1.int32)])] of ASTNode - it_parses "case\nwhen true\n1\nend", [Case.new(nil, [When.new([true.bool] of ASTNode, 1.int32)] of When)] - it_parses "case;when true;1;end", [Case.new(nil, [When.new([true.bool] of ASTNode, 1.int32)] of When)] + it_parses "case 1; end", Case.new(1.int32, [] of When, else: nil, exhaustive: false) + it_parses "case foo; end", Case.new("foo".call, [] of When, else: nil, exhaustive: false) + it_parses "case\nend", Case.new(nil, [] of When, else: nil, exhaustive: false) + it_parses "case;end", Case.new(nil, [] of When, else: nil, exhaustive: false) + it_parses "case 1\nelse\n2\nend", Case.new(1.int32, [] of When, 2.int32, exhaustive: false) + it_parses "a = 1\ncase 1\nwhen a then 1\nend", [Assign.new("a".var, 1.int32), Case.new(1.int32, [When.new(["a".var] of ASTNode, 1.int32)], else: nil, exhaustive: false)] of ASTNode + it_parses "case\nwhen true\n1\nend", Case.new(nil, [When.new([true.bool] of ASTNode, 1.int32)] of When, else: nil, exhaustive: false) + it_parses "case;when true;1;end", Case.new(nil, [When.new([true.bool] of ASTNode, 1.int32)] of When, else: nil, exhaustive: false) + + it_parses "case 1\nin Int32; 2; end", Case.new(1.int32, [When.new(["Int32".path] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case 1\nin Int32.class; 2; end", Case.new(1.int32, [When.new([Call.new("Int32".path, "class")] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case 1\nin Foo(Int32); 2; end", Case.new(1.int32, [When.new([Generic.new("Foo".path, ["Int32".path] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case 1\nin false; 2; end", Case.new(1.int32, [When.new([false.bool] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case 1\nin true; 2; end", Case.new(1.int32, [When.new([true.bool] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case 1\nin nil; 2; end", Case.new(1.int32, [When.new([NilLiteral.new] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case 1\nin .bar?; 2; end", Case.new(1.int32, [When.new([Call.new(ImplicitObj.new, "bar?")] of ASTNode, 2.int32)], else: nil, exhaustive: true) + + it_parses "case {1}\nin {Int32}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new(["Int32".path] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {Int32.class}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([Call.new("Int32".path, "class")] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {Foo(Int32)}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([Generic.new("Foo".path, ["Int32".path] of ASTNode)] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {false}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([false.bool] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {true}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([true.bool] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {nil}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([NilLiteral.new] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {.bar?}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([Call.new(ImplicitObj.new, "bar?")] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + it_parses "case {1}\nin {_}; 2; end", Case.new(TupleLiteral.new([1.int32] of ASTNode), [When.new([TupleLiteral.new([Underscore.new] of ASTNode)] of ASTNode, 2.int32)], else: nil, exhaustive: true) + + assert_syntax_error "case 1\nin Int32; 2; when 2", "expected 'in', not 'when'" + assert_syntax_error "case 1\nwhen Int32; 2; in 2", "expected 'when', not 'in'" + assert_syntax_error "case 1\nin Int32; 2; else", "exhaustive case (case ... in) doesn't allow an 'else'" + assert_syntax_error "case 1\nin 1; 2", "expression of exhaustive case (case ... in) must be a constant (like `IO::Memory`), a generic (like `Array(Int32)`) a bool literal (true or false), a nil literal (nil) or a question method (like `.red?`)" + assert_syntax_error "case 1\nin _;", "'when _' is not supported" it_parses "select\nwhen foo\n2\nend", Select.new([Select::When.new("foo".call, 2.int32)]) it_parses "select\nwhen foo\n2\nwhen bar\n4\nend", Select.new([Select::When.new("foo".call, 2.int32), Select::When.new("bar".call, 4.int32)]) @@ -1200,6 +1245,8 @@ module Crystal it_parses "[] of ->\n", ArrayLiteral.new(of: ProcNotation.new) it_parses "->foo=", ProcPointer.new(nil, "foo=") it_parses "foo = 1; ->foo.foo=", [Assign.new("foo".var, 1.int32), ProcPointer.new("foo".var, "foo=")] + it_parses "->@foo.foo", [ProcPointer.new("@foo".instance_var, "foo")] + it_parses "->@@foo.foo", [ProcPointer.new("@@foo".class_var, "foo")] it_parses "foo &->bar", Call.new(nil, "foo", block_arg: ProcPointer.new(nil, "bar")) @@ -1473,7 +1520,7 @@ module Crystal it_parses "call(foo : A, end : B)", Call.new(nil, "call", [TypeDeclaration.new("foo".var, "A".path), TypeDeclaration.new("end".var, "B".path)] of ASTNode) it_parses "call foo : A, end : B", Call.new(nil, "call", [TypeDeclaration.new("foo".var, "A".path), TypeDeclaration.new("end".var, "B".path)] of ASTNode) - it_parses "case :foo; when :bar; 2; end", Case.new("foo".symbol, [When.new(["bar".symbol] of ASTNode, 2.int32)]) + it_parses "case :foo; when :bar; 2; end", Case.new("foo".symbol, [When.new(["bar".symbol] of ASTNode, 2.int32)], else: nil, exhaustive: false) it_parses "Foo.foo(count: 3).bar { }", Call.new(Call.new("Foo".path, "foo", named_args: [NamedArgument.new("count", 3.int32)]), "bar", block: Block.new) @@ -1737,6 +1784,14 @@ module Crystal assert_syntax_error %(def foo("bar");end), "expected argument internal name" + it_parses "{[] of Foo, Bar::Baz.new}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new(Path.new(%w[Bar Baz]), "new")] of ASTNode) + it_parses "{[] of Foo, ::Bar::Baz.new}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new(Path.new(%w[Bar Baz], global: true), "new")] of ASTNode) + it_parses "{[] of Foo, Bar::Baz + 2}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new(Path.new(%w[Bar Baz]), "+", [2.int32] of ASTNode)] of ASTNode) + it_parses "{[] of Foo, Bar::Baz * 2}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new(Path.new(%w[Bar Baz]), "*", [2.int32] of ASTNode)] of ASTNode) + it_parses "{[] of Foo, Bar::Baz ** 2}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new(Path.new(%w[Bar Baz]), "**", [2.int32] of ASTNode)] of ASTNode) + it_parses "{[] of Foo, ::foo}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new(nil, "foo", global: true)] of ASTNode) + it_parses "{[] of Foo, self.foo}", TupleLiteral.new([ArrayLiteral.new([] of ASTNode, "Foo".path), Call.new("self".var, "foo")] of ASTNode) + describe "end locations" do assert_end_location "nil" assert_end_location "false" @@ -1858,6 +1913,17 @@ module Crystal it_parses %(annotation Foo\nend\nrequire "bar"), [AnnotationDef.new("Foo".path), Require.new("bar")] + assert_syntax_error "def foo(x : *Int32); end", "invalid type splat" + assert_syntax_error "def foo(x : (*Int32)); end", "invalid type splat" + assert_syntax_error "def foo(x : Int32, Int32); end" + assert_syntax_error "def foo(x : (Int32, Int32)); end" + assert_syntax_error "def foo(x : (Int32, Int32) | Int32); end" + assert_syntax_error "def foo(x : Int32 | (Int32, Int32)); end" + assert_syntax_error "def foo(x : {Int32, (Int32, Int32)}); end" + assert_syntax_error "def foo(x : 1); end" + assert_syntax_error "def foo(x : {sizeof(Int32), 2}); end" + assert_syntax_error "def foo(x : Array({sizeof(Int32), 2})); end" + it "gets corrects of ~" do node = Parser.parse("\n ~1") loc = node.location.not_nil! @@ -1957,6 +2023,12 @@ module Crystal name_location.line_number.should eq(1) name_location.column_number.should eq(12) end + + it "doesn't override yield with macro yield" do + parser = Parser.new("def foo; yield 1; {% begin %} yield 1 {% end %}; end") + a_def = parser.parse.as(Def) + a_def.yields.should eq(1) + end end end end diff --git a/spec/compiler/parser/to_s_spec.cr b/spec/compiler/parser/to_s_spec.cr index 9d01a867107d..5621376e3314 100644 --- a/spec/compiler/parser/to_s_spec.cr +++ b/spec/compiler/parser/to_s_spec.cr @@ -132,6 +132,7 @@ describe "ASTNode#to_s" do expect_to_s %((1 <= 2) <= 3) expect_to_s %(1 <= (2 <= 3)) expect_to_s %(case 1; when .foo?; 2; end), %(case 1\nwhen .foo?\n 2\nend) + expect_to_s %(case 1; in .foo?; 2; end), %(case 1\nin .foo?\n 2\nend) expect_to_s %({(1 + 2)}) expect_to_s %({foo: (1 + 2)}) expect_to_s %q("#{(1 + 2)}") @@ -169,4 +170,6 @@ describe "ASTNode#to_s" do expect_to_s "offsetof(Foo, @bar)" expect_to_s "def foo(**options, &block)\nend" expect_to_s "macro foo\n 123\nend" + expect_to_s "if true\n( 1)\nend" + expect_to_s "begin\n( 1)\nrescue\nend" end diff --git a/spec/compiler/semantic/abstract_def_spec.cr b/spec/compiler/semantic/abstract_def_spec.cr index 1f95e94da679..c0b078f6b893 100644 --- a/spec/compiler/semantic/abstract_def_spec.cr +++ b/spec/compiler/semantic/abstract_def_spec.cr @@ -395,8 +395,8 @@ describe "Semantic: abstract def" do ) end - it "warning if missing return type" do - assert_warning %( + it "errors if missing return type" do + assert_error <<-CR, abstract class Foo abstract def foo : Int32 end @@ -406,12 +406,12 @@ describe "Semantic: abstract def" do 1 end end - ), - "warning in line 8\nWarning: this method overrides Foo#foo() which has an explicit return type of Int32.\n\nPlease add an explicit return type (Int32 or a subtype of it) to this method as well." + CR + "this method overrides Foo#foo() which has an explicit return type of Int32.\n\nPlease add an explicit return type (Int32 or a subtype of it) to this method as well." end - it "warning if different return type" do - assert_warning %( + it "errors if different return type" do + assert_error <<-CR, abstract class Foo abstract def foo : Int32 end @@ -424,8 +424,8 @@ describe "Semantic: abstract def" do 1 end end - ), - "warning in line 11\nWarning: this method must return Int32, which is the return type of the overridden method Foo#foo(), or a subtype of it, not Bar::Int32" + CR + "this method must return Int32, which is the return type of the overridden method Foo#foo(), or a subtype of it, not Bar::Int32" end it "can return a more specific type" do @@ -542,8 +542,8 @@ describe "Semantic: abstract def" do )) end - it "is missing a return type in subclass of generic subclass" do - assert_warning %( + it "errors if missing a return type in subclass of generic subclass" do + assert_error <<-CR, abstract class Foo(T) abstract def foo : T end @@ -552,12 +552,12 @@ describe "Semantic: abstract def" do def foo end end - ), - "warning in line 8\nWarning: this method overrides Foo(T)#foo() which has an explicit return type of T.\n\nPlease add an explicit return type (Int32 or a subtype of it) to this method as well." + CR + "this method overrides Foo(T)#foo() which has an explicit return type of T.\n\nPlease add an explicit return type (Int32 or a subtype of it) to this method as well." end - it "can't find parent return type" do - assert_warning %( + it "errors if can't find parent return type" do + assert_error <<-CR, abstract class Foo abstract def foo : Unknown end @@ -566,12 +566,12 @@ describe "Semantic: abstract def" do def foo end end - ), - "warning in line 4\nWarning: can't resolve return type Unknown" + CR + "can't resolve return type Unknown" end - it "can't find child return type" do - assert_warning %( + it "errors if can't find child return type" do + assert_error <<-CR, abstract class Foo abstract def foo : Int32 end @@ -580,8 +580,8 @@ describe "Semantic: abstract def" do def foo : Unknown end end - ), - "warning in line 8\nWarning: can't resolve return type Unknown" + CR + "can't resolve return type Unknown" end it "doesn't crash when abstract method is implemented by supertype (#8031)" do @@ -636,4 +636,349 @@ describe "Semantic: abstract def" do end )) end + + it "can implement even if yield comes later in macro code" do + semantic(%( + module Moo + abstract def each(& : Int32 -> _) + end + + class Foo + include Moo + + def each + yield 1 + + {% if true %} + yield 2 + {% end %} + end + end + )) + end + + it "can implement by block signature even if yield comes later in macro code" do + semantic(%( + module Moo + abstract def each(& : Int32 -> _) + end + + class Foo + include Moo + + def each(& : Int32 -> _) + {% if true %} + yield 2 + {% end %} + end + end + )) + end + + it "doesn't error if implementation have default value" do + semantic %( + abstract class Foo + abstract def foo(x) + end + + class Bar < Foo + def foo(x = 1) + end + end + ) + end + + it "errors if implementation doesn't have default value" do + assert_error %( + abstract class Foo + abstract def foo(x = 1) + end + + class Bar < Foo + def foo(x) + end + end + ), + "abstract `def Foo#foo(x = 1)` must be implemented by Bar" + end + + it "errors if implementation doesn't have the same default value" do + assert_error %( + abstract class Foo + abstract def foo(x = 1) + end + + class Bar < Foo + def foo(x = 2) + end + end + ), + "abstract `def Foo#foo(x = 1)` must be implemented by Bar" + end + + it "errors if implementation doesn't have keyword arguments" do + assert_error %( + abstract class Foo + abstract def foo(*, x) + end + + class Bar < Foo + def foo(a = 0, b = 0) + end + end + ), + "abstract `def Foo#foo(*, x)` must be implemented by Bar" + end + + it "errors if implementation doesn't have a keyword argument" do + assert_error %( + abstract class Foo + abstract def foo(*, x) + end + + class Bar < Foo + def foo(*, y) + end + end + ), + "abstract `def Foo#foo(*, x)` must be implemented by Bar" + end + + it "doesn't error if implementation matches keyword argument" do + semantic %( + abstract class Foo + abstract def foo(*, x) + end + + class Bar < Foo + def foo(*, x) + end + end + ) + end + + it "errors if implementation doesn't match keyword argument type" do + assert_error %( + abstract class Foo + abstract def foo(*, x : Int32) + end + + class Bar < Foo + def foo(*, x : String) + end + end + ), + "abstract `def Foo#foo(*, x : Int32)` must be implemented by Bar" + end + + it "doesn't error if implementation have keyword arguments in different order" do + semantic %( + abstract class Foo + abstract def foo(*, x : Int32, y : String) + end + + class Bar < Foo + def foo(*, y : String, x : Int32) + end + end + ) + end + + it "errors if implementation has more keyword arguments" do + assert_error %( + abstract class Foo + abstract def foo(*, x) + end + + class Bar < Foo + def foo(*, x, y) + end + end + ), + "abstract `def Foo#foo(*, x)` must be implemented by Bar" + end + + it "doesn't error if implementation has more keyword arguments with default values" do + semantic %( + abstract class Foo + abstract def foo(*, x) + end + + class Bar < Foo + def foo(*, x, y = 1) + end + end + ) + end + + it "errors if implementation doesn't have a splat" do + assert_error %( + abstract class Foo + abstract def foo(*args) + end + + class Bar < Foo + def foo(x = 1) + end + end + ), + "abstract `def Foo#foo(*args)` must be implemented by Bar" + end + + it "errors if implementation doesn't match splat type" do + assert_error %( + abstract class Foo + abstract def foo(*args : Int32) + end + + class Bar < Foo + def foo(*args : String) + end + end + ), + "abstract `def Foo#foo(*args : Int32)` must be implemented by Bar" + end + + it "doesn't error with splat" do + semantic %( + abstract class Foo + abstract def foo(*args) + end + + class Bar < Foo + def foo(*args) + end + end + ) + end + + it "doesn't error with splat and args with default value" do + semantic %( + abstract class Foo + abstract def foo(*args) + end + + class Bar < Foo + def foo(a = 1, *args) + end + end + ) + end + + it "allows arguments to be collapsed into splat" do + semantic %( + abstract class Foo + abstract def foo(a : Int32, b : String) + end + + class Bar < Foo + def foo(*args : Int32 | String) + end + end + ) + end + + it "errors if keyword argument doesn't have the same default value" do + assert_error %( + abstract class Foo + abstract def foo(*, foo = 1) + end + + class Bar < Foo + def foo(*, foo = 2) + end + end + ), "abstract `def Foo#foo(*, foo = 1)` must be implemented by Bar" + end + + it "allow double splat argument" do + semantic %( + abstract class Foo + abstract def foo(**kargs) + end + + class Bar < Foo + def foo(**kargs) + end + end + ) + end + + it "allow double splat when abstract doesn't have it" do + semantic %( + abstract class Foo + abstract def foo + end + + class Bar < Foo + def foo(**kargs) + end + end + ) + end + + it "errors if implementation misses the double splat" do + assert_error %( + abstract class Foo + abstract def foo(**kargs) + end + + class Bar < Foo + def foo + end + end + ), "abstract `def Foo#foo(**kargs)` must be implemented by Bar" + end + + it "errors if double splat type doesn't match" do + assert_error %( + abstract class Foo + abstract def foo(**kargs : Int32) + end + + class Bar < Foo + def foo(**kargs : String) + end + end + ), "abstract `def Foo#foo(**kargs : Int32)` must be implemented by Bar" + end + + it "allow splat instead of keyword argument" do + semantic %( + abstract class Foo + abstract def foo(*, foo) + end + + class Bar < Foo + def foo(**kargs) + end + end + ) + end + + it "extra keyword arguments must have compatible type to double splat" do + assert_error %( + abstract class Foo + abstract def foo(**kargs : String) + end + + class Bar < Foo + def foo(*, foo : Int32 = 0, **kargs) + end + end + ), "abstract `def Foo#foo(**kargs : String)` must be implemented by Bar" + end + + it "double splat must match keyword argument type" do + assert_error %( + abstract class Foo + abstract def foo(*, foo : Int32) + end + + class Bar < Foo + def foo(**kargs : String) + end + end + ), "abstract `def Foo#foo(*, foo : Int32)` must be implemented by Bar" + end end diff --git a/spec/compiler/semantic/annotation_spec.cr b/spec/compiler/semantic/annotation_spec.cr index f9596fa36db4..c6aa7131bccb 100644 --- a/spec/compiler/semantic/annotation_spec.cr +++ b/spec/compiler/semantic/annotation_spec.cr @@ -730,7 +730,7 @@ describe "Semantic: annotation" do end def foo - {% if @type.instance_vars.first.annotations(Foo) %} + {% if @type.instance_vars.first.annotation(Foo) %} 1 {% else %} 'a' @@ -918,6 +918,23 @@ describe "Semantic: annotation" do end end + it "errors when annotate instance variable in subclass" do + assert_error %( + annotation Foo + end + + class Base + @x : Nil + end + + class Child < Base + @[Foo] + @x : Nil + end + ), + "can't annotate @x in Child because it was first defined in Base" + end + it "errors if wanting to add type inside annotation (1) (#8614)" do assert_error %( annotation Ann diff --git a/spec/compiler/semantic/automatic_cast.cr b/spec/compiler/semantic/automatic_cast_spec.cr similarity index 81% rename from spec/compiler/semantic/automatic_cast.cr rename to spec/compiler/semantic/automatic_cast_spec.cr index d1010287e66c..bdb0a02f3d8e 100644 --- a/spec/compiler/semantic/automatic_cast.cr +++ b/spec/compiler/semantic/automatic_cast_spec.cr @@ -492,4 +492,108 @@ describe "Semantic: automatic cast" do fill(0, 0) )) { float64 } end + + it "can autocast to union in default value" do + assert_type(%( + def fill(x : Int64 | String = 1) + x + end + + fill() + )) { int64 } + end + + it "can autocast to alias in default value" do + assert_type(%( + alias X = Int64 | String + + def fill(x : X = 1) + x + end + + fill() + )) { int64 } + end + + it "can autocast to union in default value (symbol and int)" do + assert_type(%( + enum Color + Red + end + + def fill(x : Int64 | Color = :red) + x + end + + fill() + )) { types["Color"] } + end + + it "can autocast to union in default value (multiple enums)" do + assert_type(%( + enum Color + Red + end + + enum AnotherColor + Blue + end + + def fill(x : Color | AnotherColor = :blue) + x + end + + fill() + )) { types["AnotherColor"] } + end + + it "doesn't do multidispatch if an overload matches exactly (#8217)" do + assert_type(%( + abstract class Foo + end + + class Bar < Foo + def foo(x : Int64) + x + end + + def foo(*xs : Int64) + xs + end + end + + class Baz < Foo + def foo(x : Int64) + x + end + + def foo(*xs : Int64) + xs + end + end + + Baz.new.as(Foo).foo(1) + )) { int64 } + end + + it "doesn't autocast number on union (#8655)" do + assert_type(%( + def foo(x : UInt8 | Int32, y : Float64) + x + end + + foo(255, 60) + )) { int32 } + end + + it "says ambiguous call on union (#8655)" do + assert_error %( + def foo(x : UInt64 | Int64, y : Float64) + x + end + + foo(255, 60) + ), + "ambiguous call, implicit cast of 255 matches all of UInt64, Int64" + end end diff --git a/spec/compiler/semantic/case_spec.cr b/spec/compiler/semantic/case_spec.cr index 548fb5551bf7..8a6300ddc4a6 100644 --- a/spec/compiler/semantic/case_spec.cr +++ b/spec/compiler/semantic/case_spec.cr @@ -1,48 +1,75 @@ require "../../spec_helper" describe "semantic: case" do - it "checks exhaustiveness of union type" do - assert_error %( + it "doesn't check ehxaustiveness when using 'when'" do + assert_no_errors %( a = 1 || nil case a when Int32 end - ), - "case is not exhaustive.\n\nMissing types:\n - Nil" + ) end it "checks exhaustiveness of single type" do assert_error %( case 1 - when Nil + in Nil end ), "case is not exhaustive.\n\nMissing types:\n - Int32" end - it "covers all types" do + it "checks exhaustiveness of single type (T.class)" do assert_no_errors %( - a = 1 || nil - case a - when Int32 - when Nil + case Int32 + in Int32.class end ) end - it "can't prove exhaustiveness" do - assert_error %( - struct Int32 - def ===(other) - true - end + it "checks exhaustiveness of single type (Foo(T).class)" do + assert_no_errors %( + class Foo(T) end - case 1 - when 2 + case Foo(Int32) + in Foo(Int32).class + end + ) + end + + it "checks exhaustiveness of single type (generic)" do + assert_no_errors %( + class Foo(T) + end + + case Foo(Int32).new + in Foo(Int32) + end + ) + end + + it "errors if casing against a constant" do + assert_error %( + #{bool_case_eq} + + FOO = false + + case true + in FOO end ), - "can't prove case is exhaustive.\n\nPlease add an `else` clause." + "can't use constant values in exhaustive case, only constant types" + end + + it "covers all types" do + assert_no_errors %( + a = 1 || nil + case a + in Int32 + in Nil + end + ) end it "checks exhaustiveness of bool type (missing true)" do @@ -50,7 +77,7 @@ describe "semantic: case" do #{bool_case_eq} case false - when false + in false end ), "case is not exhaustive.\n\nMissing cases:\n - true" @@ -61,7 +88,7 @@ describe "semantic: case" do #{bool_case_eq} case false - when true + in true end ), "case is not exhaustive.\n\nMissing cases:\n - false" @@ -79,7 +106,7 @@ describe "semantic: case" do e = Color::Red case e - when .red? + in .red? end ), "case is not exhaustive for enum Color.\n\nMissing members:\n - Green\n - Blue" @@ -97,7 +124,7 @@ describe "semantic: case" do e = Color::Red case e - when Color::Red + in Color::Red end ), "case is not exhaustive for enum Color.\n\nMissing members:\n - Green\n - Blue" @@ -115,9 +142,9 @@ describe "semantic: case" do e = Color::Red case e - when .red? - when .green? - when .blue? + in .red? + in .green? + in .blue? end ) end @@ -137,9 +164,9 @@ describe "semantic: case" do end case foo - when .red? - when .green? - when .blue? + in .red? + in .green? + in .blue? end ) end @@ -149,9 +176,9 @@ describe "semantic: case" do #{bool_case_eq} case 1 || true - when Int32 - when true - when false + in Int32 + in true + in false end ) end @@ -166,13 +193,13 @@ describe "semantic: case" do a = 1 || Foo.new || Bar.new case a - when Foo + in Foo end ), "case is not exhaustive.\n\nMissing types:\n - Int32" end - it "checks exhaustiveness, covers when base type covers" do + it "checks exhaustiveness, covers in base type covers" do assert_no_errors %( class Foo end @@ -182,19 +209,19 @@ describe "semantic: case" do a = Bar.new case a - when Foo + in Foo end ) end - it "checks exhaustiveness, covers when base type covers (generic type)" do + it "checks exhaustiveness, covers in base type covers (generic type)" do assert_no_errors %( class Foo(T) end a = Foo(Int32).new case a - when Foo + in Foo end ) end @@ -208,7 +235,7 @@ describe "semantic: case" do end case nil - when nil + in nil end ) end @@ -223,22 +250,16 @@ describe "semantic: case" do a = 1 || nil case a - when nil - when Int32 - end - ) - end - - it "never warns on condless case without else" do - assert_no_errors %( - case - when 1 == 2 + in nil + in Int32 end ) end - it "always requires an else for Flags enum (no coverage)" do + it "can't prove case is exhaustive for @[Flags] enum" do assert_error %( + #{enum_eq} + struct Enum def includes?(other : self) false @@ -254,14 +275,42 @@ describe "semantic: case" do e = Color::Red case e - when .red? + in .red? end ), - "can't prove case is exhaustive.\n\nPlease add an `else` clause." + <<-ERROR + case is not exhaustive. + + Missing cases: + - Color + + Note that @[Flags] enum can't be proved to be exhaustive by matching against enum members. + In particular, the enum Color can't be proved to be exhaustive like that. + ERROR + end + + it "can prove case is exhaustive for @[Flags] enum when matching type" do + assert_no_errors %( + require "prelude" + + @[Flags] + enum Color + Red + Green + Blue + end + + e = Color::Red + case e + in Color + end + ) end - it "always requires an else for Flags enum (all members covered but doesn't count)" do + it "can't prove case is exhaustive for @[Flags] enum, tuple case" do assert_error %( + #{enum_eq} + struct Enum def includes?(other : self) false @@ -276,13 +325,19 @@ describe "semantic: case" do end e = Color::Red - case e - when .red? - when .green? - when .blue? + case {e} + in {.red?} end ), - "can't prove case is exhaustive.\n\nPlease add an `else` clause." + <<-ERROR + case is not exhaustive. + + Missing cases: + - {Color} + + Note that @[Flags] enum can't be proved to be exhaustive by matching against enum members. + In particular, the enum Color can't be proved to be exhaustive like that. + ERROR end it "checks exhaustiveness of enum combined with another type" do @@ -297,8 +352,8 @@ describe "semantic: case" do e = Color::Red || 1 case e - when Int32 - when .red? + in Int32 + in .red? end ), "case is not exhaustive for enum Color.\n\nMissing members:\n - Green\n - Blue" @@ -310,7 +365,7 @@ describe "semantic: case" do e = 1 || true case e - when true + in true end ), "case is not exhaustive.\n\nMissing cases:\n - false\n - Int32" @@ -322,10 +377,10 @@ describe "semantic: case" do b = 1 || 'a' case {a, b} - when {Int32, Char} - when {Int32, Int32} - when {Char, Int32} - when {Char, Char} + in {Int32, Char} + in {Int32, Int32} + in {Char, Int32} + in {Char, Char} end ) end @@ -335,9 +390,9 @@ describe "semantic: case" do a = 1 || 'a' case {a, a} - when {Int32, Char} - when {Int32, Int32} - when {Char, Char} + in {Int32, Char} + in {Int32, Int32} + in {Char, Char} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Int32}" @@ -348,12 +403,12 @@ describe "semantic: case" do a = 1 || 'a' case {a, a, a} - when {Int32, Int32, Int32} - when {Int32, Char, Int32} - when {Int32, Char, Char} - when {Char, Int32, Int32} - when {Char, Char, Int32} - when {Char, Char, Char} + in {Int32, Int32, Int32} + in {Int32, Char, Int32} + in {Int32, Char, Char} + in {Char, Int32, Int32} + in {Char, Char, Int32} + in {Char, Char, Char} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Int32, Char}\n - {Int32, Int32, Char}" @@ -364,7 +419,7 @@ describe "semantic: case" do #{bool_case_eq} case {true, 'a'} - when {true, Char} + in {true, Char} end ), "case is not exhaustive.\n\nMissing cases:\n - {false, Char}" @@ -381,8 +436,8 @@ describe "semantic: case" do end case {Color::Red, 'a'} - when {.red?, Char} - when {.blue?, Char} + in {.red?, Char} + in {.blue?, Char} end ), "case is not exhaustive.\n\nMissing cases:\n - {Color::Green, Char}" @@ -393,7 +448,7 @@ describe "semantic: case" do a = 1 || 'a' case {a, a} - when {_, Int32} + in {_, Int32} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Char}\n - {Int32, Char}" @@ -404,7 +459,7 @@ describe "semantic: case" do a = 1 || 'a' case {a, a} - when {Int32, _} + in {Int32, _} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Char}\n - {Char, Int32}" @@ -415,7 +470,7 @@ describe "semantic: case" do #{bool_case_eq} case {true, 1 || 'a'} - when {_, Int32} + in {_, Int32} end ), "case is not exhaustive.\n\nMissing cases:\n - {Bool, Char}" @@ -426,8 +481,8 @@ describe "semantic: case" do #{bool_case_eq} case {true, 1 || 'a'} - when {_, Int32} - when {false, Char} + in {_, Int32} + in {false, Char} end ), "case is not exhaustive.\n\nMissing cases:\n - {true, Char}" @@ -438,7 +493,7 @@ describe "semantic: case" do #{bool_case_eq} case {1 || 'a', true} - when {Int32, _} + in {Int32, _} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Bool}" @@ -449,8 +504,8 @@ describe "semantic: case" do #{bool_case_eq} case {1 || 'a', true} - when {Int32, _} - when {Char, false} + in {Int32, _} + in {Char, false} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, true}" @@ -467,7 +522,7 @@ describe "semantic: case" do end case {Color::Red, 1 || 'a'} - when {_, Int32} + in {_, Int32} end ), "case is not exhaustive.\n\nMissing cases:\n - {Color, Char}" @@ -484,8 +539,8 @@ describe "semantic: case" do end case {Color::Red, 1 || 'a'} - when {_, Int32} - when {.blue?, Char} + in {_, Int32} + in {.blue?, Char} end ), "case is not exhaustive.\n\nMissing cases:\n - {Color::Red, Char}\n - {Color::Green, Char}" @@ -502,7 +557,7 @@ describe "semantic: case" do end case {1 || 'a', Color::Red} - when {Int32, _} + in {Int32, _} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Color}" @@ -519,8 +574,8 @@ describe "semantic: case" do end case {1 || 'a', Color::Red} - when {Int32, _} - when {Char, .blue?} + in {Int32, _} + in {Char, .blue?} end ), "case is not exhaustive.\n\nMissing cases:\n - {Char, Color::Red}\n - {Char, Color::Green}" @@ -537,10 +592,10 @@ describe "semantic: case" do foo = 1 case {foo.bar, foo.bar} - when {Int32, Char} - when {Int32, Int32} - when {Char, Int32} - when {Char, Char} + in {Int32, Char} + in {Int32, Int32} + in {Char, Int32} + in {Char, Char} end ) end diff --git a/spec/compiler/semantic/cast_spec.cr b/spec/compiler/semantic/cast_spec.cr index d9568c7058b2..881d042eefdd 100644 --- a/spec/compiler/semantic/cast_spec.cr +++ b/spec/compiler/semantic/cast_spec.cr @@ -359,4 +359,14 @@ describe "Semantic: cast" do x.foo if x )) { nil_type } end + + it "considers else to be unreachable (#9658)" do + assert_type(%( + case 1 + in Int32 + v = 1 + end + v + )) { int32 } + end end diff --git a/spec/compiler/semantic/class_var_spec.cr b/spec/compiler/semantic/class_var_spec.cr index 1a266f14bf6c..f263b2abd6d3 100644 --- a/spec/compiler/semantic/class_var_spec.cr +++ b/spec/compiler/semantic/class_var_spec.cr @@ -437,7 +437,7 @@ describe "Semantic: class var" do )) { int32 } end - it "doesn't error on recursive depdendency if var is nilable (#2943)" do + it "doesn't error on recursive dependency if var is nilable (#2943)" do assert_type(%( class Foo @@foo : Int32? diff --git a/spec/compiler/semantic/cleanup_spec.cr b/spec/compiler/semantic/cleanup_spec.cr index 0e7704c78833..a221b45275ba 100644 --- a/spec/compiler/semantic/cleanup_spec.cr +++ b/spec/compiler/semantic/cleanup_spec.cr @@ -1,5 +1,11 @@ require "../../spec_helper" +private def assert_after_cleanup(before, after) + node = Parser.parse(before) + result = semantic node + result.node.to_s.strip.should eq(after.strip) +end + describe "cleanup" do it "errors if assigning var to itself" do assert_error "a = 1; a = a", "expression has no effect" diff --git a/spec/compiler/semantic/closure_spec.cr b/spec/compiler/semantic/closure_spec.cr index 69f26dec829f..809ebca9fda1 100644 --- a/spec/compiler/semantic/closure_spec.cr +++ b/spec/compiler/semantic/closure_spec.cr @@ -524,4 +524,29 @@ describe "Semantic: closure" do ), "can't send closure to C function (closured vars: x)" end + + it "doesn't closure typeof local var" do + result = assert_type("x = 1; -> { typeof(x) }; x") { int32 } + program = result.program + var = program.vars["x"] + var.closured?.should be_false + end + + it "doesn't closure typeof instance var (#9479)" do + result = assert_type(" + class Foo + @x : Int32? + + def foo + -> { typeof(@x) } + end + end + + Foo.new.foo + 1 + ") { int32 } + node = result.node.as(Expressions) + call = node.expressions[-2].as(Call) + call.target_def.self_closured?.should be_false + end end diff --git a/spec/compiler/semantic/concrete_types_spec.cr b/spec/compiler/semantic/concrete_types_spec.cr index 4b793c3acb56..f540244e39ba 100644 --- a/spec/compiler/semantic/concrete_types_spec.cr +++ b/spec/compiler/semantic/concrete_types_spec.cr @@ -1,7 +1,7 @@ require "../../spec_helper" private def assert_concrete_types(str) - result = semantic_result("struct Witness;end\n\n#{str}", nil, inject_primitives: false) + result = semantic("struct Witness;end\n\n#{str}", inject_primitives: false) program = result.program type, expected_concrete_types = yield program.types, program diff --git a/spec/compiler/semantic/doc_spec.cr b/spec/compiler/semantic/doc_spec.cr index d8006f0af7fc..f4ce5a0d11c6 100644 --- a/spec/compiler/semantic/doc_spec.cr +++ b/spec/compiler/semantic/doc_spec.cr @@ -148,6 +148,21 @@ describe "Semantic: doc" do bar.doc.should eq("Hello") end + it "stores doc for macro when using ditto" do + result = semantic %( + # Hello + macro bar + end + + # :ditto: + macro bar2 + end + ), wants_doc: true + program = result.program + bar2 = program.lookup_macros("bar2").as(Array(Macro)).first + bar2.doc.should eq("Hello") + end + {% for def_type in %w[def macro].map &.id %} it "overwrites doc for {{def_type}} when redefining" do result = semantic %( @@ -399,4 +414,122 @@ describe "Semantic: doc" do type = program.types["MyClass1"] type.doc.should eq("Some description") end + + it "attaches doc to annotation in macro expansion (#9628)" do + result = semantic %( + macro ann + annotation MyAnnotation + end + end + + # Some description + ann + ), wants_doc: true + program = result.program + type = program.types["MyAnnotation"] + type.doc.should eq("Some description") + end + + context "doc before annotation" do + it "attached to struct/class" do + result = semantic %( + # Some description + @[Packed] + struct Foo + end + ), wants_doc: true + program = result.program + type = program.types["Foo"] + type.doc.should eq("Some description") + end + + it "attached to module" do + result = semantic %( + annotation Ann + end + + # Some description + @[Ann] + module Foo + end + ), wants_doc: true + program = result.program + type = program.types["Foo"] + type.doc.should eq("Some description") + end + + it "attached to enum" do + result = semantic %( + annotation Ann + end + + # Some description + @[Ann] + enum Foo + One + end + ), wants_doc: true + program = result.program + type = program.types["Foo"] + type.doc.should eq("Some description") + end + + it "attached to constant" do + result = semantic %( + annotation Ann + end + + # Some description + @[Ann] + Foo = 1 + ), wants_doc: true + program = result.program + type = program.types["Foo"] + type.doc.should eq("Some description") + end + + it "attached to alias" do + result = semantic %( + annotation Ann + end + + # Some description + @[Ann] + alias Foo = Int32 + ), wants_doc: true + program = result.program + type = program.types["Foo"] + type.doc.should eq("Some description") + end + + it "attached to def" do + result = semantic %( + annotation Ann + end + + # Some description + @[Ann] + def foo + end + ), wants_doc: true + program = result.program + a_def = program.lookup_defs("foo").first + a_def.doc.should eq("Some description") + end + + it "attached to macro" do + result = semantic %( + annotation Ann + end + + # Some description + @[Ann] + macro foo + end + ), wants_doc: true + program = result.program + type = program.lookup_macros("foo").as(Array(Macro)).first + type.doc.should eq("Some description") + end + end end diff --git a/spec/compiler/semantic/generic_class_spec.cr b/spec/compiler/semantic/generic_class_spec.cr index 93aac06adf11..3725488a727e 100644 --- a/spec/compiler/semantic/generic_class_spec.cr +++ b/spec/compiler/semantic/generic_class_spec.cr @@ -479,7 +479,7 @@ describe "Semantic: generic class" do "use a more specific type" end - it "errors on too nested generic instance" do + pending_win32 "errors on too nested generic instance" do assert_error %( class Foo(T) end @@ -493,7 +493,7 @@ describe "Semantic: generic class" do "generic type too nested" end - it "errors on too nested generic instance, with union type" do + pending_win32 "errors on too nested generic instance, with union type" do assert_error %( class Foo(T) end @@ -507,7 +507,7 @@ describe "Semantic: generic class" do "generic type too nested" end - it "errors on too nested tuple instance" do + pending_win32 "errors on too nested tuple instance" do assert_error %( def foo {typeof(foo)} diff --git a/spec/compiler/semantic/hooks_spec.cr b/spec/compiler/semantic/hooks_spec.cr index dd471027bbd1..a2ff7ee7d0aa 100644 --- a/spec/compiler/semantic/hooks_spec.cr +++ b/spec/compiler/semantic/hooks_spec.cr @@ -228,4 +228,24 @@ describe "Semantic: hooks" do {a1.x, a2.y} ), inject_primitives: false) { tuple_of([string, int32]) } end + + it "does inherited macro through generic instance type (#9693)" do + assert_type(" + class Foo(X) + macro inherited + def self.{{@type.name.downcase.id}} + 1 + end + end + end + + class Bar < Foo(Int32) + end + + class Baz < Bar + end + + Baz.baz + ") { int32 } + end end diff --git a/spec/compiler/semantic/lib_spec.cr b/spec/compiler/semantic/lib_spec.cr index b9d3131093c9..9e8feabd1b30 100644 --- a/spec/compiler/semantic/lib_spec.cr +++ b/spec/compiler/semantic/lib_spec.cr @@ -345,7 +345,7 @@ describe "Semantic: lib" do lib LibFoo end ), - "unknown link argument: 'boo' (valid arguments are 'lib', 'ldflags', 'static' and 'framework')" + "unknown link argument: 'boo' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config' and 'framework')" end it "errors if lib already specified with positional argument" do @@ -376,6 +376,24 @@ describe "Semantic: lib" do )) { int32 } end + it "warns if @[Link(static: true)] is specified" do + assert_warning <<-CR, + @[Link("foo", static: true)] + lib Foo + end + CR + "warning in line 1\nWarning: specifying static linking for individual libraries is deprecated" + end + + it "warns if Link annotations use positional arguments" do + assert_warning <<-CR, + @[Link("foo", "bar")] + lib Foo + end + CR + "warning in line 1\nWarning: using non-named arguments for Link annotations is deprecated" + end + it "allows invoking lib call without obj inside lib" do assert_type(%( lib LibFoo diff --git a/spec/compiler/semantic/macro_spec.cr b/spec/compiler/semantic/macro_spec.cr index 3d595e37e8ba..90bdd6497fc0 100644 --- a/spec/compiler/semantic/macro_spec.cr +++ b/spec/compiler/semantic/macro_spec.cr @@ -1461,6 +1461,20 @@ describe "Semantic: macro" do ), inject_primitives: false) method = result.program.types["Foo"].lookup_first_def("bar", false).not_nil! - method.location.not_nil!.original_location.not_nil!.line_number.should eq(10) + method.location.not_nil!.expanded_location.not_nil!.line_number.should eq(10) + end + + it "executes OpAssign (#9356)" do + assert_type(%( + {% begin %} + {% a = nil %} + {% a ||= 1 %} + {% if a %} + 1 + {% else %} + 'a' + {% end %} + {% end %} + )) { int32 } end end diff --git a/spec/compiler/semantic/nilable_cast_spec.cr b/spec/compiler/semantic/nilable_cast_spec.cr index c2ddac878cd0..7382bf42c43c 100644 --- a/spec/compiler/semantic/nilable_cast_spec.cr +++ b/spec/compiler/semantic/nilable_cast_spec.cr @@ -47,4 +47,25 @@ describe "Semantic: nilable cast" do end )) { string } end + + it "casts to module" do + assert_type(%( + module Moo + end + + class Base + end + + class Foo < Base + include Moo + end + + class Bar < Base + include Moo + end + + base = (Foo.new || Bar.new) + base.as?(Moo) + )) { union_of([types["Foo"], types["Bar"], nil_type] of Type) } + end end diff --git a/spec/compiler/semantic/primitives_spec.cr b/spec/compiler/semantic/primitives_spec.cr index 9b9555b73363..fcf826540b96 100644 --- a/spec/compiler/semantic/primitives_spec.cr +++ b/spec/compiler/semantic/primitives_spec.cr @@ -224,7 +224,7 @@ describe "Semantic: primitives" do "method marked as Primitive must have an empty body" end - it "types va_arg primitive" do + pending_win32 "types va_arg primitive" do assert_type(%( struct VaList @[Primitive(:va_arg)] diff --git a/spec/compiler/semantic/proc_spec.cr b/spec/compiler/semantic/proc_spec.cr index 03b0c7fae9d9..701517c024ca 100644 --- a/spec/compiler/semantic/proc_spec.cr +++ b/spec/compiler/semantic/proc_spec.cr @@ -1069,4 +1069,42 @@ describe "Semantic: proc" do foo )) { proc_of nil_type } end + + it "can use @ivar as pointer syntax receiver (#9239)" do + assert_type(%( + class Foo + def foo + 1 + end + end + + class Bar + @foo = Foo.new + + def foo + ->@foo.foo + end + end + + Bar.new.foo + )) { proc_of int32 } + end + + it "can use @@cvar as pointer syntax receiver (#9239)" do + assert_type(%( + class Foo + @@foo = new + + def self.foo + ->@@foo.foo + end + + def foo + 1 + end + end + + Foo.foo + )) { proc_of int32 } + end end diff --git a/spec/compiler/semantic/reflection_spec.cr b/spec/compiler/semantic/reflection_spec.cr index f9bb33d8119d..2d79a19f6b61 100644 --- a/spec/compiler/semantic/reflection_spec.cr +++ b/spec/compiler/semantic/reflection_spec.cr @@ -9,7 +9,7 @@ describe "Semantic: reflection" do assert_type("Class") { types["Class"] } end - it "types Object and Class metaclases" do + it "types Object and Class metaclasses" do assert_type("Object.class") { types["Class"] } assert_type("Class.class") { types["Class"] } end diff --git a/spec/compiler/semantic/require_spec.cr b/spec/compiler/semantic/require_spec.cr index 82d9c1210721..bfbffb43f015 100644 --- a/spec/compiler/semantic/require_spec.cr +++ b/spec/compiler/semantic/require_spec.cr @@ -1,10 +1,37 @@ require "../../spec_helper" describe "Semantic: require" do - it "raises crystal exception if can't find require (#7385)" do - node = parse(%(require "file_that_doesnt_exist")) - expect_raises ::Crystal::Exception do - semantic(node) + describe "file not found" do + it "require" do + error = assert_error %(require "file_that_doesnt_exist"), + "can't find file 'file_that_doesnt_exist'", + inject_primitives: false + + error.message.not_nil!.should contain "If you're trying to require a shard:" + end + + it "relative require" do + error = assert_error %(require "./file_that_doesnt_exist"), + "can't find file './file_that_doesnt_exist'", + inject_primitives: false + + error.message.not_nil!.should_not contain "If you're trying to require a shard:" + end + + it "wildcard" do + error = assert_error %(require "file_that_doesnt_exist/*"), + "can't find file 'file_that_doesnt_exist/*'", + inject_primitives: false + + error.message.not_nil!.should contain "If you're trying to require a shard:" + end + + it "relative wildcard" do + error = assert_error %(require "./file_that_doesnt_exist/*"), + "can't find file './file_that_doesnt_exist/*'", + inject_primitives: false + + error.message.not_nil!.should_not contain "If you're trying to require a shard:" end end end diff --git a/spec/compiler/semantic/restrictions_spec.cr b/spec/compiler/semantic/restrictions_spec.cr index 00784dea5c1b..2542fab3ab95 100644 --- a/spec/compiler/semantic/restrictions_spec.cr +++ b/spec/compiler/semantic/restrictions_spec.cr @@ -680,16 +680,6 @@ describe "Restrictions" do )) { types["Parent"].metaclass.virtual_type! } end - it "doesn't crash on invalid splat restriction (#3698)" do - assert_error %( - def foo(arg : *String) - end - - foo(1) - ), - "no overload matches" - end - it "errors if using free var without forall" do assert_error %( def foo(x : T) diff --git a/spec/compiler/codegen/warnings_spec.cr b/spec/compiler/semantic/warnings_spec.cr similarity index 50% rename from spec/compiler/codegen/warnings_spec.cr rename to spec/compiler/semantic/warnings_spec.cr index 65d26833d3e4..4230df9f4113 100644 --- a/spec/compiler/codegen/warnings_spec.cr +++ b/spec/compiler/semantic/warnings_spec.cr @@ -1,30 +1,30 @@ require "../spec_helper" -describe "Code gen: warnings" do +describe "Semantic: warnings" do it "detects top-level deprecated methods" do - assert_warning %( + assert_warning <<-CR, @[Deprecated("Do not use me")] def foo end foo - ), "warning in line 6\nWarning: Deprecated top-level foo. Do not use me", - inject_primitives: false + CR + "warning in line 5\nWarning: Deprecated top-level foo. Do not use me" end it "deprecation reason is optional" do - assert_warning %( + assert_warning <<-CR, @[Deprecated] def foo end foo - ), "warning in line 6\nWarning: Deprecated top-level foo.", - inject_primitives: false + CR + "warning in line 5\nWarning: Deprecated top-level foo." end it "detects deprecated instance methods" do - assert_warning %( + assert_warning <<-CR, class Foo @[Deprecated("Do not use me")] def m @@ -32,12 +32,12 @@ describe "Code gen: warnings" do end Foo.new.m - ), "warning in line 8\nWarning: Deprecated Foo#m. Do not use me", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo#m. Do not use me" end it "detects deprecated class methods" do - assert_warning %( + assert_warning <<-CR, class Foo @[Deprecated("Do not use me")] def self.m @@ -45,12 +45,12 @@ describe "Code gen: warnings" do end Foo.m - ), "warning in line 8\nWarning: Deprecated Foo.m. Do not use me", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo.m. Do not use me" end it "detects deprecated generic instance methods" do - assert_warning %( + assert_warning <<-CR, class Foo(T) @[Deprecated("Do not use me")] def m @@ -58,12 +58,12 @@ describe "Code gen: warnings" do end Foo(Int32).new.m - ), "warning in line 8\nWarning: Deprecated Foo(Int32)#m. Do not use me", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo(Int32)#m. Do not use me" end it "detects deprecated generic class methods" do - assert_warning %( + assert_warning <<-CR, class Foo(T) @[Deprecated("Do not use me")] def self.m @@ -71,12 +71,12 @@ describe "Code gen: warnings" do end Foo(Int32).m - ), "warning in line 8\nWarning: Deprecated Foo(Int32).m. Do not use me", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo(Int32).m. Do not use me" end it "detects deprecated module methods" do - assert_warning %( + assert_warning <<-CR, module Foo @[Deprecated("Do not use me")] def self.m @@ -84,23 +84,23 @@ describe "Code gen: warnings" do end Foo.m - ), "warning in line 8\nWarning: Deprecated Foo.m. Do not use me", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo.m. Do not use me" end it "detects deprecated methods with named arguments" do - assert_warning %( + assert_warning <<-CR, @[Deprecated] def foo(*, a) end foo(a: 2) - ), "warning in line 6\nWarning: Deprecated top-level foo:a.", - inject_primitives: false + CR + "warning in line 5\nWarning: Deprecated top-level foo:a." end it "detects deprecated initialize" do - assert_warning %( + assert_warning <<-CR, class Foo @[Deprecated] def initialize @@ -108,12 +108,12 @@ describe "Code gen: warnings" do end Foo.new - ), "warning in line 8\nWarning: Deprecated Foo.new.", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo.new." end it "detects deprecated initialize with named arguments" do - assert_warning %( + assert_warning <<-CR, class Foo @[Deprecated] def initialize(*, a) @@ -121,12 +121,12 @@ describe "Code gen: warnings" do end Foo.new(a: 2) - ), "warning in line 8\nWarning: Deprecated Foo.new:a.", - inject_primitives: false + CR + "warning in line 7\nWarning: Deprecated Foo.new:a." end it "informs warnings once per call site location (a)" do - warning_failures = warnings_result %( + warning_failures = warnings_result <<-CR class Foo @[Deprecated("Do not use me")] def m @@ -139,13 +139,12 @@ describe "Code gen: warnings" do Foo.new.b Foo.new.b - ), inject_primitives: false - + CR warning_failures.size.should eq(1) end it "informs warnings once per call site location (b)" do - warning_failures = warnings_result %( + warning_failures = warnings_result <<-CR class Foo @[Deprecated("Do not use me")] def m @@ -154,13 +153,13 @@ describe "Code gen: warnings" do Foo.new.m Foo.new.m - ), inject_primitives: false + CR warning_failures.size.should eq(2) end it "informs warnings once per yield" do - warning_failures = warnings_result %( + warning_failures = warnings_result <<-CR class Foo @[Deprecated("Do not use me")] def m @@ -173,13 +172,13 @@ describe "Code gen: warnings" do end twice { Foo.new.m } - ), inject_primitives: false + CR warning_failures.size.should eq(1) end it "informs warnings once per target type" do - warning_failures = warnings_result %( + warning_failures = warnings_result <<-CR class Foo(T) @[Deprecated("Do not use me")] def m @@ -192,7 +191,174 @@ describe "Code gen: warnings" do Foo(Int32).new.b Foo(Int64).new.b - ), inject_primitives: false + CR + + warning_failures.size.should eq(2) + end + + it "ignore deprecation excluded locations" do + with_tempfile("check_warnings_excludes") do |path| + FileUtils.mkdir_p File.join(path, "lib") + + # NOTE tempfile might be created in symlinked folder + # which affects how to match current dir /var/folders/... + # with the real path /private/var/folders/... + path = File.real_path(path) + + main_filename = File.join(path, "main.cr") + output_filename = File.join(path, "main") + + Dir.cd(path) do + File.write main_filename, <<-CR + require "./lib/foo" + + bar + foo + CR + File.write File.join(path, "lib", "foo.cr"), <<-CR + @[Deprecated("Do not use me")] + def foo + end + + def bar + foo + end + CR + + compiler = create_spec_compiler + compiler.warnings = Warnings::All + compiler.warnings_exclude << Crystal.normalize_path "lib" + compiler.prelude = "empty" + result = compiler.compile Compiler::Source.new(main_filename, File.read(main_filename)), output_filename + + result.program.warning_failures.size.should eq(1) + end + end + end + + it "errors if invalid argument type" do + assert_error <<-CR, + @[Deprecated(42)] + def foo + end + CR + "Error: first argument must be a String" + end + + it "errors if too many arguments" do + assert_error <<-CR, + @[Deprecated("Do not use me", "extra arg")] + def foo + end + CR + "Error: wrong number of deprecated annotation arguments (given 2, expected 1)" + end + + it "errors if invalid named arguments" do + assert_error <<-CR, + @[Deprecated(invalid: "Do not use me")] + def foo + end + CR + "Error: too many named arguments (given 1, expected maximum 0)" + end + + it "detects top-level deprecated marcos" do + assert_warning %( + @[Deprecated("Do not use me")] + macro foo + end + + foo + ), "warning in line 6\nWarning: Deprecated top-level foo. Do not use me" + end + + it "deprecation reason is optional" do + assert_warning %( + @[Deprecated] + macro foo + end + + foo + ), "warning in line 6\nWarning: Deprecated top-level foo." + end + + it "detects deprecated class macros" do + assert_warning %( + class Foo + @[Deprecated("Do not use me")] + macro m + end + end + + Foo.m + ), "warning in line 8\nWarning: Deprecated Foo.m. Do not use me" + end + + it "detects deprecated generic class macros" do + assert_warning %( + class Foo(T) + @[Deprecated("Do not use me")] + macro m + end + end + + Foo.m + ), "warning in line 8\nWarning: Deprecated Foo.m. Do not use me" + end + + it "detects deprecated module macros" do + assert_warning %( + module Foo + @[Deprecated("Do not use me")] + macro m + end + end + + Foo.m + ), "warning in line 8\nWarning: Deprecated Foo.m. Do not use me" + end + + it "detects deprecated macros with named arguments" do + assert_warning %( + @[Deprecated] + macro foo(*, a) + end + + foo(a: 2) + ), "warning in line 6\nWarning: Deprecated top-level foo." + end + + it "informs warnings once per call site location (a)" do + warning_failures = warnings_result %( + class Foo + @[Deprecated("Do not use me")] + macro m + end + + macro b + Foo.m + end + end + + Foo.b + Foo.b + ) + + warning_failures.size.should eq(1) + end + + it "informs warnings once per call site location (b)" do + warning_failures = warnings_result %( + class Foo + @[Deprecated("Do not use me")] + macro m + end + end + + Foo.m + Foo.m + ) warning_failures.size.should eq(2) end @@ -218,10 +384,10 @@ describe "Code gen: warnings" do ) File.write File.join(path, "lib", "foo.cr"), %( @[Deprecated("Do not use me")] - def foo + macro foo end - def bar + macro bar foo end ) @@ -240,7 +406,7 @@ describe "Code gen: warnings" do it "errors if invalid argument type" do assert_error %( @[Deprecated(42)] - def foo + macro foo end ), "Error: first argument must be a String" @@ -249,16 +415,16 @@ describe "Code gen: warnings" do it "errors if too many arguments" do assert_error %( @[Deprecated("Do not use me", "extra arg")] - def foo + macro foo end ), "Error: wrong number of deprecated annotation arguments (given 2, expected 1)" end - it "errors if missing link arguments" do + it "errors if invalid named argument" do assert_error %( @[Deprecated(invalid: "Do not use me")] - def foo + macro foo end ), "Error: too many named arguments (given 1, expected maximum 0)" diff --git a/spec/compiler/util_spec.cr b/spec/compiler/util_spec.cr new file mode 100644 index 000000000000..9a4815a9e588 --- /dev/null +++ b/spec/compiler/util_spec.cr @@ -0,0 +1,20 @@ +require "spec" +require "compiler/crystal/util" + +describe Crystal do + describe "normalize_path" do + sep = {{ flag?(:win32) ? "\\" : "/" }} + + it { Crystal.normalize_path("a").should eq ".#{sep}a" } + it { Crystal.normalize_path("./a/b").should eq ".#{sep}a#{sep}b" } + it { Crystal.normalize_path("../a/b").should eq ".#{sep}..#{sep}a#{sep}b" } + it { Crystal.normalize_path("/foo/bar").should eq "#{sep}foo#{sep}bar" } + + {% if flag?(:win32) %} + it { Crystal.normalize_path("C:\\foo\\bar").should eq "C:\\foo\\bar" } + it { Crystal.normalize_path("C:foo\\bar").should eq "C:foo\\bar" } + it { Crystal.normalize_path("\\foo\\bar").should eq "\\foo\\bar" } + it { Crystal.normalize_path("foo\\bar").should eq ".\\foo\\bar" } + {% end %} + end +end diff --git a/spec/generate_windows_spec.sh b/spec/generate_windows_spec.sh index 046b6b64ca23..3cb1f4ebf0f0 100644 --- a/spec/generate_windows_spec.sh +++ b/spec/generate_windows_spec.sh @@ -6,7 +6,7 @@ set +x # * `failed codegen` annotates specs that error in the compiler. # This is mostly caused by some API not being ported to win32 (either the spec # target itself or some tools used by the spec). -# * `failed linking` annotats specs that compile but don't link (at least not on +# * `failed linking` annotates specs that compile but don't link (at least not on # basis of the libraries from *Porting to Windows* guide). # Most failers are caused by missing libraries (libxml2, libyaml, libgmp, # libllvm, libz, libssl), but there also seem to be some incompatibilities diff --git a/spec/manual/digest_large_file_spec.cr b/spec/manual/digest_large_file_spec.cr new file mode 100644 index 000000000000..419deea1b589 --- /dev/null +++ b/spec/manual/digest_large_file_spec.cr @@ -0,0 +1,27 @@ +require "spec" +require "digest/sha1" +require "digest/md5" + +private DATA = "a" * 1024 +private TOTAL_SIZE_GB = 1 +private TOTAL_SIZE = TOTAL_SIZE_GB * 1024 * 1024 * 1024 + +describe Digest::SHA1 do + it "does digest for large file" do + Digest::SHA1.digest do |ctx| + (TOTAL_SIZE / DATA.size).ceil.to_i.times do + ctx.update DATA + end + end + end +end + +describe Digest::MD5 do + it "does digest for large file" do + Digest::MD5.digest do |ctx| + (TOTAL_SIZE / DATA.size).ceil.to_i.times do + ctx.update DATA + end + end + end +end diff --git a/spec/manual/find_executable_spec.cr b/spec/manual/find_executable_spec.cr new file mode 100644 index 000000000000..ed7781b033cc --- /dev/null +++ b/spec/manual/find_executable_spec.cr @@ -0,0 +1,58 @@ +# Verifies that find_executable's specs match the behavior of Process.run. +# This doesn't actually test find_executable, only takes all the test cases +# directly from spec/std/process/find_executable_spec.cr and checks that +# *they* match what the OS actually does when finding an executable for the +# purpose of running it. + +require "spec" +require "digest/sha1" +require "../support/env" +require "../support/tempfile" +require "../std/process/find_executable_spec" + +describe "Process.run" do + test_dir = Path[SPEC_TEMPFILE_PATH] / "manual_find_executable" + base_dir = Path[test_dir] / "base" + path_dir = Path[test_dir] / "path" + + around_all do |all| + Dir.mkdir_p(test_dir) + + exe_names, non_exe_names = FIND_EXECUTABLE_TEST_FILES + exe_names.each do |name| + src_fn = test_dir / "self_printer.cr" + exe_fn = test_dir / "self_printer.exe" + File.write(src_fn, "print #{name.inspect}") + Process.run("bin/crystal", ["build", "-o", exe_fn.to_s, src_fn.to_s]) + Dir.mkdir_p((base_dir / name).parent) + File.rename(exe_fn, base_dir / name) + end + non_exe_names.each do |name| + File.write(base_dir / name, "") + end + + with_env "PATH": {ENV["PATH"], path_dir}.join(Process::PATH_DELIMITER) do + Dir.cd(base_dir) do + all.run + end + end + + FileUtils.rm_r(test_dir.to_s) + end + + find_executable_test_cases(base_dir).each do |(command, exp)| + if exp + it "runs '#{command}' as '#{exp}'" do + output = Process.run command, &.output.gets_to_end + $?.success?.should be_true + output.should eq exp + end + else + it "fails to run '#{command}'" do + expect_raises IO::Error do + Process.run(command) + end + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index ed0862a1cb4c..ee121a3a6038 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -4,10 +4,10 @@ ENV["CRYSTAL_PATH"] = "#{__DIR__}/../src" require "spec" -{% skip_file if flag?(:win32) %} - require "../src/compiler/crystal/**" require "./support/syntax" +require "./support/tempfile" +require "./support/win32" class Crystal::Program def union_of(type1, type2, type3) @@ -33,82 +33,80 @@ end record SemanticResult, program : Program, - node : ASTNode, - type : Type + node : ASTNode -def assert_type(str, flags = nil, inject_primitives = true) - result = semantic_result(str, flags, inject_primitives: inject_primitives) +def assert_type(str, *, inject_primitives = true, flags = nil, file = __FILE__, line = __LINE__) + result = semantic(str, flags: flags, inject_primitives: inject_primitives) program = result.program expected_type = with program yield program - result.type.should eq(expected_type) + node = result.node + if node.is_a?(Expressions) + node = node.last + end + node.type.should eq(expected_type), file: file, line: line result end -def semantic(code : String, wants_doc = false, inject_primitives = true) - code = inject_primitives(code) if inject_primitives - semantic parse(code, wants_doc: wants_doc), wants_doc: wants_doc +def semantic(code : String, wants_doc = false, inject_primitives = true, flags = nil, filename = nil) + node = parse(code, wants_doc: wants_doc, filename: filename) + node = inject_primitives(node) if inject_primitives + semantic node, wants_doc: wants_doc, flags: flags end -def semantic(node : ASTNode, wants_doc = false) - program = new_program - program.wants_doc = wants_doc - node = program.normalize node - node = program.semantic node - SemanticResult.new(program, node, node.type) +private def inject_primitives(node : ASTNode) + req = Crystal::Require.new("primitives") + case node + when Crystal::Expressions + node.expressions.unshift req + node + when Crystal::Nop + node + else + Crystal::Expressions.new [req, node] of ASTNode + end end -def semantic_result(str, flags = nil, inject_primitives = true) - str = inject_primitives(str) if inject_primitives +def semantic(node : ASTNode, wants_doc = false, flags = nil) program = new_program program.flags.concat(flags.split) if flags - input = parse str - input = program.normalize input - input = program.semantic input - input_type = input.is_a?(Expressions) ? input.last.type : input.type - SemanticResult.new(program, input, input_type) + program.wants_doc = wants_doc + node = program.normalize node + node = program.semantic node + SemanticResult.new(program, node) end -def assert_normalize(from, to, flags = nil) +def assert_normalize(from, to, flags = nil, *, file = __FILE__, line = __LINE__) program = new_program program.flags.concat(flags.split) if flags normalizer = Normalizer.new(program) from_nodes = Parser.parse(from) to_nodes = program.normalize(from_nodes) - to_nodes.to_s.strip.should eq(to.strip) + to_nodes.to_s.strip.should eq(to.strip), file: file, line: line to_nodes end -def assert_expand(from : String, to) - assert_expand Parser.parse(from), to +def assert_expand(from : String, to, *, file = __FILE__, line = __LINE__) + assert_expand Parser.parse(from), to, file: file, line: line end -def assert_expand(from_nodes : ASTNode, to) +def assert_expand(from_nodes : ASTNode, to, *, file = __FILE__, line = __LINE__) to_nodes = LiteralExpander.new(new_program).expand(from_nodes) - to_nodes.to_s.strip.should eq(to.strip) + to_nodes.to_s.strip.should eq(to.strip), file: file, line: line end -def assert_expand_second(from : String, to) +def assert_expand_second(from : String, to, *, file = __FILE__, line = __LINE__) node = (Parser.parse(from).as(Expressions))[1] - assert_expand node, to + assert_expand node, to, file: file, line: line end -def assert_expand_third(from : String, to) +def assert_expand_third(from : String, to, *, file = __FILE__, line = __LINE__) node = (Parser.parse(from).as(Expressions))[2] - assert_expand node, to -end - -def assert_after_cleanup(before, after) - # before = inject_primitives(before) - node = Parser.parse(before) - result = semantic node - result.node.to_s.strip.should eq(after.strip) + assert_expand node, to, file: file, line: line end -def assert_error(str, message, inject_primitives = true) - str = inject_primitives(str) if inject_primitives - nodes = parse str - expect_raises TypeException, message do - semantic nodes +def assert_error(str, message, *, inject_primitives = true, file = __FILE__, line = __LINE__) + expect_raises TypeException, message, file, line do + semantic str, inject_primitives: inject_primitives end end @@ -116,63 +114,51 @@ def assert_no_errors(*args) semantic(*args) end -def warnings_result(code, inject_primitives = true) - code = inject_primitives(code) if inject_primitives - - output_filename = Crystal.temp_executable("crystal-spec-output") - +def warnings_result(code, *, file = __FILE__) compiler = create_spec_compiler compiler.warnings = Warnings::All compiler.error_on_warnings = false compiler.prelude = "empty" # avoid issues in the current std lib compiler.color = false apply_program_flags(compiler.flags) - result = compiler.compile Compiler::Source.new("code.cr", code), output_filename - result.program.warning_failures -end + with_temp_executable("crystal-spec-output", file: file) do |output_filename| + result = compiler.compile Compiler::Source.new("code.cr", code), output_filename -def assert_warning(code, message, inject_primitives = true) - warning_failures = warnings_result(code, inject_primitives) - warning_failures.size.should eq(1) - warning_failures[0].should start_with(message) + return result.program.warning_failures + end end -def assert_no_warnings(code, inject_primitives = true) - warning_failures = warnings_result(code, inject_primitives) - warning_failures.size.should eq(0) +def assert_warning(code, message, *, file = __FILE__, line = __LINE__) + warning_failures = warnings_result(code, file: file) + warning_failures.size.should eq(1), file, line + warning_failures[0].should start_with(message), file, line end -def assert_macro(macro_args, macro_body, call_args, expected, expected_pragmas = nil, flags = nil) - assert_macro(macro_args, macro_body, expected, expected_pragmas, flags) { call_args } +def assert_macro(macro_args, macro_body, call_args, expected, expected_pragmas = nil, flags = nil, file = __FILE__, line = __LINE__) + assert_macro(macro_args, macro_body, expected, expected_pragmas, flags, file: file, line: line) { call_args } end -def assert_macro(macro_args, macro_body, expected, expected_pragmas = nil, flags = nil) +def assert_macro(macro_args, macro_body, expected, expected_pragmas = nil, flags = nil, file = __FILE__, line = __LINE__) program = new_program program.flags.concat(flags.split) if flags sub_node = yield program - assert_macro_internal program, sub_node, macro_args, macro_body, expected, expected_pragmas + assert_macro_internal program, sub_node, macro_args, macro_body, expected, expected_pragmas, file: file, line: line end -def assert_macro_internal(program, sub_node, macro_args, macro_body, expected, expected_pragmas) +def assert_macro_internal(program, sub_node, macro_args, macro_body, expected, expected_pragmas, file = __FILE__, line = __LINE__) macro_def = "macro foo(#{macro_args});#{macro_body};end" a_macro = Parser.parse(macro_def).as(Macro) call = Call.new(nil, "", sub_node) result, result_pragmas = program.expand_macro a_macro, call, program, program result = result.chomp(';') - result.should eq(expected) + result.should eq(expected), file: file, line: line result_pragmas.should eq(expected_pragmas) if expected_pragmas end def codegen(code, inject_primitives = true, debug = Crystal::Debug::None, filename = __FILE__) - code = inject_primitives(code) if inject_primitives - parser = Parser.new(code) - parser.filename = filename - parser.wants_doc = false - node = parser.parse - - result = semantic node + result = semantic code, inject_primitives: inject_primitives, filename: filename result.program.codegen(result.node, single_module: false, debug: debug)[""].mod end @@ -235,8 +221,10 @@ def create_spec_compiler compiler end -def run(code, filename = nil, inject_primitives = true, debug = Crystal::Debug::None, flags = nil) - code = inject_primitives(code) if inject_primitives +def run(code, filename = nil, inject_primitives = true, debug = Crystal::Debug::None, flags = nil, *, file = __FILE__) + if inject_primitives + code = %(require "primitives"\n#{code}) + end # Code that requires the prelude doesn't run in LLVM's MCJIT # because of missing linked functions (which are available @@ -252,69 +240,29 @@ def run(code, filename = nil, inject_primitives = true, debug = Crystal::Debug:: ast.expressions[-1] = exps code = ast.to_s - output_filename = Crystal.temp_executable("crystal-spec-output") - compiler = create_spec_compiler compiler.debug = debug compiler.flags.concat flags if flags apply_program_flags(compiler.flags) - compiler.compile Compiler::Source.new("spec", code), output_filename - output = `#{output_filename}` - File.delete(output_filename) + with_temp_executable("crystal-spec-output", file: file) do |output_filename| + compiler.compile Compiler::Source.new("spec", code), output_filename - SpecRunOutput.new(output) + output = `#{Process.quote(output_filename)}` + return SpecRunOutput.new(output) + end else new_program.run(code, filename: filename, debug: debug) end end -def build(code) - code_file = File.tempname("build_and_run_code") - - # write code to the temp file - File.write(code_file, code) - - binary_file = File.tempname("build_and_run_bin") - - `bin/crystal build #{encode_program_flags} #{code_file.path.inspect} -o #{binary_file.path.inspect}` - File.exists?(binary_file).should be_true - - yield binary_file -ensure - File.delete(code_file) if code_file - File.delete(binary_file) if binary_file -end - -def build_and_run(code) - build(code) do |binary_file| - out_io, err_io = IO::Memory.new, IO::Memory.new - status = Process.run(binary_file, output: out_io, error: err_io) - - {status, out_io.to_s, err_io.to_s} - end -end - -def test_c(c_code, crystal_code) - c_filename = "#{__DIR__}/temp_abi.c" - o_filename = "#{__DIR__}/temp_abi.o" - begin - File.write(c_filename, c_code) - - `#{Crystal::Compiler::CC} #{c_filename} -c -o #{o_filename}`.should be_truthy - +def test_c(c_code, crystal_code, *, file = __FILE__) + with_temp_c_object_file(c_code, file: file) do |o_filename| yield run(%( require "prelude" - @[Link(ldflags: "#{o_filename}")] + @[Link(ldflags: #{o_filename.inspect})] #{crystal_code} )) - ensure - File.delete(c_filename) - File.delete(o_filename) end end - -private def inject_primitives(code) - %(require "primitives"\n) + code -end diff --git a/spec/std/adler32_spec.cr b/spec/std/adler32_spec.cr deleted file mode 100644 index 450ee398d3d8..000000000000 --- a/spec/std/adler32_spec.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "spec" -require "adler32" - -describe Adler32 do - it "should be able to calculate adler32" do - adler = Adler32.checksum("foo").to_s(16) - adler.should eq("2820145") - end - - it "should be able to calculate adler32 combined" do - adler1 = Adler32.checksum("hello") - adler2 = Adler32.checksum(" world!") - combined = Adler32.combine(adler1, adler2, " world!".size) - Adler32.checksum("hello world!").should eq(combined) - end -end diff --git a/spec/std/array_spec.cr b/spec/std/array_spec.cr index adb9e2d847ec..39a4d5cc65f8 100644 --- a/spec/std/array_spec.cr +++ b/spec/std/array_spec.cr @@ -588,6 +588,20 @@ describe "Array" do a.should eq([1, 3, 4]) end + it "deletes negative index with range" do + a = [1, 2, 3, 4, 5, 6] + a.delete_at(-3, 2).should eq([4, 5]) + a.should eq([1, 2, 3, 6]) + end + + it "deletes negative index with range, out of bounds" do + a = [1, 2, 3, 4, 5, 6] + + expect_raises IndexError do + a.delete_at(-7, 2) + end + end + it "deletes out of bounds" do expect_raises IndexError do [1].delete_at(2) @@ -1852,6 +1866,30 @@ describe "Array" do it { a = [1, 2, 3]; a.rotate(3001).should eq([2, 3, 1]); a.should eq([1, 2, 3]) } it { a = [1, 2, 3]; a.rotate(-1).should eq([3, 1, 2]); a.should eq([1, 2, 3]) } it { a = [1, 2, 3]; a.rotate(-3001).should eq([3, 1, 2]); a.should eq([1, 2, 3]) } + + it do + a = Array(Int32).new(50) { |i| i } + a.rotate!(5) + a.should eq([5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 0, 1, 2, 3, 4]) + end + + it do + a = Array(Int32).new(50) { |i| i } + a.rotate!(-5) + a.should eq([45, 46, 47, 48, 49, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]) + end + + it do + a = Array(Int32).new(50) { |i| i } + a.rotate!(20) + a.should eq([20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + end + + it do + a = Array(Int32).new(50) { |i| i } + a.rotate!(-20) + a.should eq([30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) + end end describe "permutations" do diff --git a/spec/std/big/big_int_spec.cr b/spec/std/big/big_int_spec.cr index 2646f75ad942..c58873757dac 100644 --- a/spec/std/big/big_int_spec.cr +++ b/spec/std/big/big_int_spec.cr @@ -434,6 +434,34 @@ describe "BigInt" do it "has unsafe_shr (#8691)" do BigInt.new(8).unsafe_shr(1).should eq(4) end + + describe "#digits" do + it "works for positive numbers or zero" do + 0.to_big_i.digits.should eq([0]) + 1.to_big_i.digits.should eq([1]) + 10.to_big_i.digits.should eq([0, 1]) + 123.to_big_i.digits.should eq([3, 2, 1]) + 123456789.to_big_i.digits.should eq([9, 8, 7, 6, 5, 4, 3, 2, 1]) + end + + it "works with a base" do + 123.to_big_i.digits(16).should eq([11, 7]) + end + + it "raises for invalid base" do + [1, 0, -1].each do |base| + expect_raises(ArgumentError, "Invalid base #{base}") do + 123.to_big_i.digits(base) + end + end + end + + it "raises for negative numbers" do + expect_raises(ArgumentError, "Can't request digits of negative number") do + -123.to_big_i.digits + end + end + end end describe "BigInt Math" do diff --git a/spec/std/bit_array_spec.cr b/spec/std/bit_array_spec.cr index 6383adeb1ad1..d0086fca63df 100644 --- a/spec/std/bit_array_spec.cr +++ b/spec/std/bit_array_spec.cr @@ -74,6 +74,10 @@ describe "BitArray" do (b == c).should be_false (a == d).should be_false end + + it "compares other type" do + from_int(3, 0b101).should_not eq("other type") + end end describe "[]" do @@ -337,4 +341,13 @@ describe "BitArray" do iter.next.should be_true iter.next.should be_a(Iterator::Stop) end + + it "provides dup" do + a = BitArray.new(2) + b = a.dup + + b[0] = true + a[0].should be_false + b[0].should be_true + end end diff --git a/spec/std/channel_spec.cr b/spec/std/channel_spec.cr index 0a82deaf0806..dff88b48eddf 100644 --- a/spec/std/channel_spec.cr +++ b/spec/std/channel_spec.cr @@ -618,11 +618,6 @@ describe "unbuffered" do Channel.select(ch1.receive_select_action, ch2.receive_select_action).should eq({0, 123}) end - it "works with select else" do - ch1 = Channel(Int32).new - Channel.select({ch1.receive_select_action}, true).should eq({1, Channel::NotReady.new}) - end - it "can send and receive nil" do ch = Channel(Nil).new sender = Fiber.new { ch.send nil } @@ -633,8 +628,9 @@ describe "unbuffered" do it "can be closed" do ch = Channel(Int32).new ch.closed?.should be_false - ch.close.should be_nil + ch.close.should be_true ch.closed?.should be_true + ch.close.should be_false expect_raises(Channel::ClosedError) { ch.receive } end diff --git a/spec/std/char/reader_spec.cr b/spec/std/char/reader_spec.cr index a395db44b640..0d34beac6d45 100644 --- a/spec/std/char/reader_spec.cr +++ b/spec/std/char/reader_spec.cr @@ -1,10 +1,10 @@ require "spec" require "char/reader" -private def assert_invalid_byte_sequence(bytes, width) +private def assert_invalid_byte_sequence(bytes) reader = Char::Reader.new(String.new bytes) reader.current_char.should eq(Char::REPLACEMENT) - reader.current_char_width.should eq(width) + reader.current_char_width.should eq(1) reader.error.should eq(bytes[0]) end @@ -130,51 +130,51 @@ describe "Char::Reader" do end it "errors if 0x80 <= first_byte < 0xC2" do - assert_invalid_byte_sequence Bytes[0x80], 1 - assert_invalid_byte_sequence Bytes[0xC1], 1 + assert_invalid_byte_sequence Bytes[0x80] + assert_invalid_byte_sequence Bytes[0xC1] end it "errors if (second_byte & 0xC0) != 0x80" do - assert_invalid_byte_sequence Bytes[0xd0], 1 + assert_invalid_byte_sequence Bytes[0xd0] end it "errors if first_byte == 0xE0 && second_byte < 0xA0" do - assert_invalid_byte_sequence Bytes[0xe0, 0x9F, 0xA0], 3 + assert_invalid_byte_sequence Bytes[0xe0, 0x9F, 0xA0] end it "errors if first_byte == 0xED && second_byte >= 0xA0" do - assert_invalid_byte_sequence Bytes[0xed, 0xB0, 0xA0], 3 + assert_invalid_byte_sequence Bytes[0xed, 0xB0, 0xA0] end it "errors if first_byte < 0xF0 && (third_byte & 0xC0) != 0x80" do - assert_invalid_byte_sequence Bytes[0xe0, 0xA0, 0], 2 + assert_invalid_byte_sequence Bytes[0xe0, 0xA0, 0] end it "errors if first_byte == 0xF0 && second_byte < 0x90" do - assert_invalid_byte_sequence Bytes[0xf0, 0x8F, 0xA0], 3 + assert_invalid_byte_sequence Bytes[0xf0, 0x8F, 0xA0] end it "errors if first_byte == 0xF4 && second_byte >= 0x90" do - assert_invalid_byte_sequence Bytes[0xf4, 0x90, 0xA0], 3 + assert_invalid_byte_sequence Bytes[0xf4, 0x90, 0xA0] end it "errors if first_byte < 0xF5 && (fourth_byte & 0xC0) != 0x80" do - assert_invalid_byte_sequence Bytes[0xf4, 0x8F, 0xA0, 0], 4 + assert_invalid_byte_sequence Bytes[0xf4, 0x8F, 0xA0, 0] end it "errors if first_byte >= 0xF5" do - assert_invalid_byte_sequence Bytes[0xf5, 0x8F, 0xA0, 0xA0], 4 + assert_invalid_byte_sequence Bytes[0xf5, 0x8F, 0xA0, 0xA0] end it "errors if second_byte is out of bounds" do - assert_invalid_byte_sequence Bytes[0xf4], 1 + assert_invalid_byte_sequence Bytes[0xf4] end it "errors if third_byte is out of bounds" do - assert_invalid_byte_sequence Bytes[0xf4, 0x8f], 2 + assert_invalid_byte_sequence Bytes[0xf4, 0x8f] end it "errors if fourth_byte is out of bounds" do - assert_invalid_byte_sequence Bytes[0xf4, 0x8f, 0xa0], 3 + assert_invalid_byte_sequence Bytes[0xf4, 0x8f, 0xa0] end end diff --git a/spec/std/colorize_spec.cr b/spec/std/colorize_spec.cr index 772eae3f4d76..bfe39f9613da 100644 --- a/spec/std/colorize_spec.cr +++ b/spec/std/colorize_spec.cr @@ -129,32 +129,6 @@ describe "colorize" do colorize("hello", :red).inspect.should eq("\e[31m\"hello\"\e[0m") end - describe "with_color deprecated top-level method" do - it "without args" do - io = IO::Memory.new - with_color.red.toggle(true).surround(io) do - io << "hello" - with_color.green.toggle(true).surround(io) do - io << "world" - end - io << "bye" - end - io.to_s.should eq("\e[31mhello\e[0;32mworld\e[0;31mbye\e[0m") - end - - it "with args" do - io = IO::Memory.new - with_color(:red).toggle(true).surround(io) do - io << "hello" - with_color(:green).toggle(true).surround(io) do - io << "world" - end - io << "bye" - end - io.to_s.should eq("\e[31mhello\e[0;32mworld\e[0;31mbye\e[0m") - end - end - it "colorizes with surround" do io = IO::Memory.new with_color_wrap.red.surround(io) do diff --git a/spec/std/complex_spec.cr b/spec/std/complex_spec.cr index 568961d4d7f6..ddad2a571cac 100644 --- a/spec/std/complex_spec.cr +++ b/spec/std/complex_spec.cr @@ -118,7 +118,7 @@ describe "Complex" do describe "+" do it "+ complex" do - (+Complex.new(-5.43, -27.12)).should eq(Complex.new(5.43, 27.12)) + (+Complex.new(-5.43, -27.12)).should eq(Complex.new(-5.43, -27.12)) end it "complex + complex" do diff --git a/spec/std/crc32_spec.cr b/spec/std/crc32_spec.cr deleted file mode 100644 index cfc5ef1b0853..000000000000 --- a/spec/std/crc32_spec.cr +++ /dev/null @@ -1,16 +0,0 @@ -require "spec" -require "crc32" - -describe CRC32 do - it "should be able to calculate crc32" do - crc = CRC32.checksum("foo").to_s(16) - crc.should eq("8c736521") - end - - it "should be able to calculate crc32 combined" do - crc1 = CRC32.checksum("hello") - crc2 = CRC32.checksum(" world!") - combined = CRC32.combine(crc1, crc2, " world!".size) - CRC32.checksum("hello world!").should eq(combined) - end -end diff --git a/spec/std/data/static_file_handler/test.txt.gz b/spec/std/data/static_file_handler/test.txt.gz new file mode 100644 index 000000000000..25923f999dbd Binary files /dev/null and b/spec/std/data/static_file_handler/test.txt.gz differ diff --git a/spec/std/deque_spec.cr b/spec/std/deque_spec.cr index 0bef3b5b0960..fb6a43eb60f4 100644 --- a/spec/std/deque_spec.cr +++ b/spec/std/deque_spec.cr @@ -145,6 +145,15 @@ describe "Deque" do (b == c).should be_false (a == d).should be_false end + + it "compares other types" do + a = Deque{1, 2, 3} + b = Deque{:foo, :bar} + c = "other type" + (a == b).should be_false + (b == c).should be_false + (a == c).should be_false + end end describe "+" do diff --git a/spec/std/digest/md5_spec.cr b/spec/std/digest/md5_spec.cr index 2f6afb8f4e3f..7421f4625470 100644 --- a/spec/std/digest/md5_spec.cr +++ b/spec/std/digest/md5_spec.cr @@ -1,7 +1,10 @@ require "spec" +require "./spec_helper" require "digest/md5" describe Digest::MD5 do + it_acts_as_digest_algorithm Digest::MD5 + it "calculates digest from string" do Digest::MD5.digest("foo").to_slice.should eq Bytes[0xac, 0xbd, 0x18, 0xdb, 0x4c, 0xc2, 0xf8, 0x5c, 0xed, 0xef, 0x65, 0x4f, 0xcc, 0xc4, 0xa4, 0xd8] end @@ -34,4 +37,26 @@ describe Digest::MD5 do it "calculates base64'd hash from string" do Digest::MD5.base64digest("foo").should eq("rL0Y20zC+Fzt72VPzMSk2A==") end + + it "resets" do + digest = Digest::MD5.new + digest.update "foo" + digest.final.hexstring.should eq("acbd18db4cc2f85cedef654fccc4a4d8") + + digest.reset + digest.update "foo" + digest.final.hexstring.should eq("acbd18db4cc2f85cedef654fccc4a4d8") + end + + it "can't call final twice" do + digest = Digest::MD5.new + digest.final + expect_raises(Digest::FinalizedError) do + digest.final + end + end + + it "return the digest size" do + Digest::MD5.new.digest_size.should eq 16 + end end diff --git a/spec/std/digest/sha1_spec.cr b/spec/std/digest/sha1_spec.cr index 99a13e1548fe..6f793c2cbde3 100644 --- a/spec/std/digest/sha1_spec.cr +++ b/spec/std/digest/sha1_spec.cr @@ -1,7 +1,10 @@ require "spec" +require "./spec_helper" require "digest/sha1" describe Digest::SHA1 do + it_acts_as_digest_algorithm Digest::SHA1 + [ {"", "da39a3ee5e6b4b0d3255bfef95601890afd80709", "2jmj7l5rSw0yVb/vlWAYkK/YBwk="}, {"The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", "L9ThxnotKPzthJ7hu3bnORuT6xI="}, @@ -10,10 +13,28 @@ describe Digest::SHA1 do {"a", "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8", "hvfkN/qlp/zhXR3cuerq6jd2Z7g="}, {"0123456701234567012345670123456701234567012345670123456701234567", "e0c094e867ef46c350ef54a7f59dd60bed92ae83", "4MCU6GfvRsNQ71Sn9Z3WC+2SroM="}, {"fooø", "dcf4a1e3542b1a40a4ac2a3f7c92ffdb2d19812f", "3PSh41QrGkCkrCo/fJL/2y0ZgS8="}, - ].each do |(string, hexdigest, base64digest)| + ].each do |(string, hexstring, base64digest)| it "does digest for #{string.inspect}" do bytes = Digest::SHA1.digest(string) - bytes.to_slice.hexstring.should eq(hexdigest) + bytes.hexstring.should eq(hexstring) + end + + it "resets" do + digest = Digest::SHA1.new + digest.update string + digest.final.hexstring.should eq(hexstring) + + digest.reset + digest.update string + digest.final.hexstring.should eq(hexstring) + end + + it "can't call #final more than once" do + digest = Digest::SHA1.new + digest.final + expect_raises(Digest::FinalizedError) do + digest.final + end end it "does digest for #{string.inspect} in a block" do @@ -23,15 +44,19 @@ describe Digest::SHA1 do end end - bytes.to_slice.hexstring.should eq(hexdigest) + bytes.hexstring.should eq(hexstring) end it "does hexdigest for #{string.inspect}" do - Digest::SHA1.hexdigest(string).should eq(hexdigest) + Digest::SHA1.hexdigest(string).should eq(hexstring) end it "does base64digest for #{string.inspect}" do Digest::SHA1.base64digest(string).should eq(base64digest) end end + + it "returns the digest_size" do + Digest::SHA1.new.digest_size.should eq(20) + end end diff --git a/spec/std/digest/spec_helper.cr b/spec/std/digest/spec_helper.cr new file mode 100644 index 000000000000..d54db274605e --- /dev/null +++ b/spec/std/digest/spec_helper.cr @@ -0,0 +1,47 @@ +def it_acts_as_digest_algorithm(type : T.class) forall T + it "#hexdigest can update within a loop from explicit expr (#9483)" do + i = 0 + type.hexdigest do |digest| + while i < 3 + digest.update("") + i += 1 + end + end + end + + pending "#hexdigest can update within a loop by indirect expr (#9483)" do + algorithm = {} of String => Digest::Base.class + algorithm["me"] = type + i = 0 + algorithm["me"].hexdigest do |digest| + while i < 3 + digest.update("") + i += 1 + end + end + end + + it "context are independent" do + algorithm = type + res = algorithm.hexdigest do |digest| + digest.update("a") + digest.update("b") + end + + inner_res = nil + + outer_res = algorithm.hexdigest do |outer| + outer.update("a") + + inner_res = algorithm.hexdigest do |inner| + inner.update("a") + inner.update("b") + end + + outer.update("b") + end + + outer_res.should eq(res) + inner_res.should eq(res) + end +end diff --git a/spec/std/dir_spec.cr b/spec/std/dir_spec.cr index 5a0394d0599a..282c68f28e31 100644 --- a/spec/std/dir_spec.cr +++ b/spec/std/dir_spec.cr @@ -75,6 +75,15 @@ describe "Dir" do end end + it "tests mkdir and delete with a new path" do + with_tempfile("mkdir") do |path| + Dir.mkdir(path, 0o700) + Dir.exists?(path).should be_true + Dir.delete(path) + Dir.exists?(path).should be_false + end + end + it "tests mkdir and rmdir with a new path" do with_tempfile("mkdir") do |path| Dir.mkdir(path, 0o700) @@ -116,17 +125,17 @@ describe "Dir" do end end - it "tests rmdir with an nonexistent path" do + it "tests delete with an nonexistent path" do with_tempfile("nonexistant") do |path| expect_raises(File::NotFoundError, "Unable to remove directory: '#{path.inspect_unquoted}'") do - Dir.rmdir(path) + Dir.delete(path) end end end - it "tests rmdir with a path that cannot be removed" do + it "tests delete with a path that cannot be removed" do expect_raises(File::Error, "Unable to remove directory: '#{datapath.inspect_unquoted}'") do - Dir.rmdir(datapath) + Dir.delete(datapath) end end @@ -359,6 +368,24 @@ describe "Dir" do Dir.glob("#{datapath}/dir/dots/**/*", match_hidden: false).size.should eq 0 end end + + context "with path" do + expected = [ + datapath("dir", "f1.txt"), + datapath("dir", "f2.txt"), + datapath("dir", "g2.txt"), + ] + + it "posix path" do + Dir[Path.posix(datapath, "dir", "*.txt")].sort.should eq expected + Dir[[Path.posix(datapath, "dir", "*.txt")]].sort.should eq expected + end + + it "windows path" do + Dir[Path.windows(datapath, "dir", "*.txt")].sort.should eq expected + Dir[[Path.windows(datapath, "dir", "*.txt")]].sort.should eq expected + end + end end describe "cd" do @@ -536,8 +563,8 @@ describe "Dir" do Dir.mkdir_p("foo\0bar") end - it_raises_on_null_byte "rmdir" do - Dir.rmdir("foo\0bar") + it_raises_on_null_byte "delete" do + Dir.delete("foo\0bar") end end end diff --git a/spec/std/enumerable_spec.cr b/spec/std/enumerable_spec.cr index 18dda442134b..1ca3cc3dace4 100644 --- a/spec/std/enumerable_spec.cr +++ b/spec/std/enumerable_spec.cr @@ -17,6 +17,17 @@ private class SpecEmptyEnumerable end end +private class SpecCountUpIterator + include Iterator(Int32) + + def initialize(@size : Int32, @count = 0) + end + + def next + (@count += 1) <= @size ? (@count - 1) : stop + end +end + describe "Enumerable" do describe "all? with block" do it "returns true" do @@ -492,16 +503,6 @@ describe "Enumerable" do end end - describe "grep" do - it "works with regexes for instance" do - ["Alice", "Bob", "Cipher", "Anna"].grep(/^A/).should eq ["Alice", "Anna"] - end - - it "returns empty array if nothing matches" do - %w(Alice Bob Mallory).grep(/nothing/).should eq [] of String - end - end - describe "group_by" do it { [1, 2, 2, 3].group_by { |x| x == 2 }.should eq({true => [2, 2], false => [1, 3]}) } @@ -620,26 +621,60 @@ describe "Enumerable" do end end - describe "join" do - it "joins with separator and block" do - str = [1, 2, 3].join(", ") { |x| x + 1 } - str.should eq("2, 3, 4") + describe "#join" do + it "()" do + [1, 2, 3].join.should eq("123") + end + + it "(separator)" do + ["Ruby", "Crystal", "Python"].join(", ").should eq "Ruby, Crystal, Python" end - it "joins without separator and block" do + it "(&)" do str = [1, 2, 3].join { |x| x + 1 } str.should eq("234") end - it "joins with io and block" do + it "(separator, &)" do + str = [1, 2, 3].join(", ") { |x| x + 1 } + str.should eq("2, 3, 4") + end + + it "(io)" do + io = IO::Memory.new + [1, 2, 3].join(io) + io.to_s.should eq("123") + end + + it "(io, separator)" do + io = IO::Memory.new + ["Ruby", "Crystal", "Python"].join(io, ", ") + io.to_s.should eq "Ruby, Crystal, Python" + end + + it "(separator, io) (deprecated)" do + io = IO::Memory.new + ["Ruby", "Crystal", "Python"].join(", ", io) + io.to_s.should eq "Ruby, Crystal, Python" + end + + it "(io, &)" do + io = IO::Memory.new + [1, 2, 3].join(io) { |x, io| io << x + 1 } + io.to_s.should eq("234") + end + + it "(io, separator, &)" do + io = IO::Memory.new + [1, 2, 3].join(io, ", ") { |x, io| io << x + 1 } + io.to_s.should eq("2, 3, 4") + end + + it "(separator, io, &) (deprecated)" do str = IO::Memory.new [1, 2, 3].join(", ", str) { |x, io| io << x + 1 } str.to_s.should eq("2, 3, 4") end - - it "joins with only separator" do - ["Ruby", "Crystal", "Python"].join(", ").should eq "Ruby, Crystal, Python" - end end describe "map" do @@ -1006,4 +1041,16 @@ describe "Enumerable" do (1..3).to_h { |i| {i, i ** 2} }.should eq({1 => 1, 2 => 4, 3 => 9}) end end + + describe "zip" do + it "works for Iterators as receiver" do + SpecCountUpIterator.new(3).zip(1..3, 2..4).should eq([{0, 1, 2}, {1, 2, 3}, {2, 3, 4}]) + end + end + + describe "zip?" do + it "works for Iterators as receiver" do + SpecCountUpIterator.new(3).zip?(1..2, 2..4).should eq([{0, 1, 2}, {1, 2, 3}, {2, nil, 4}]) + end + end end diff --git a/spec/std/env_spec.cr b/spec/std/env_spec.cr index bd7e062c0d69..7465aea27128 100644 --- a/spec/std/env_spec.cr +++ b/spec/std/env_spec.cr @@ -20,6 +20,23 @@ describe "ENV" do ENV.delete("FOO") end + {% if flag?(:win32) %} + it "sets and gets case-insensitive" do + (ENV["FOO"] = "1").should eq("1") + ENV["Foo"].should eq("1") + ENV["foo"]?.should eq("1") + ensure + ENV.delete("FOO") + end + {% else %} + it "sets and gets case-sensitive" do + ENV["FOO"] = "1" + ENV["foo"]?.should be_nil + ensure + ENV.delete("FOO") + end + {% end %} + it "sets to nil (same as delete)" do ENV["FOO"] = "1" ENV["FOO"]?.should_not be_nil @@ -58,6 +75,12 @@ describe "ENV" do ENV.delete("BAR") end + it "does not have an empty key" do + # Setting an empty key is invalid on both POSIX and Windows. So reporting an empty key + # would always be a bug. And there *was* a bug - see win32/ Crystal::System::Env.each + ENV.keys.should_not contain("") + end + it "does .values" do [1, 2].each { |i| ENV.values.should_not contain("SOMEVALUE_#{i}") } ENV["FOO"] = "SOMEVALUE_1" diff --git a/spec/std/exception/call_stack_spec.cr b/spec/std/exception/call_stack_spec.cr index efb1bd5f68bc..81ef86ad7333 100644 --- a/spec/std/exception/call_stack_spec.cr +++ b/spec/std/exception/call_stack_spec.cr @@ -1,19 +1,7 @@ require "../spec_helper" -private def compile_and_run_file(source_file) - with_tempfile("executable_file") do |executable_file| - Process.run("bin/crystal", ["build", "--debug", "-o", executable_file, source_file]) - File.exists?(executable_file).should be_true - - output, error = IO::Memory.new, IO::Memory.new - Process.run executable_file, output: output, error: error - - {output.to_s, error.to_s} - end -end - describe "Backtrace" do - it "prints file line:colunm" do + pending_win32 "prints file line:colunm" do source_file = datapath("backtrace_sample") # CallStack tries to make files relative to the current dir, @@ -22,7 +10,7 @@ describe "Backtrace" do current_dir += File::SEPARATOR unless current_dir.ends_with?(File::SEPARATOR) source_file = source_file.lchop(current_dir) - output, _ = compile_and_run_file(source_file) + _, output, _ = compile_and_run_file(source_file) # resolved file line:column output.should match(/#{source_file}:3:10 in 'callee1'/) @@ -40,16 +28,16 @@ describe "Backtrace" do it "prints exception backtrace to stderr" do sample = datapath("exception_backtrace_sample") - output, error = compile_and_run_file(sample) + _, output, error = compile_and_run_file(sample) output.to_s.empty?.should be_true error.to_s.should contain("IndexError") end - it "prints crash backtrace to stderr" do + pending_win32 "prints crash backtrace to stderr" do sample = datapath("crash_backtrace_sample") - output, error = compile_and_run_file(sample) + _, output, error = compile_and_run_file(sample) output.to_s.empty?.should be_true error.to_s.should contain("Invalid memory access") diff --git a/spec/std/exception_spec.cr b/spec/std/exception_spec.cr index 72840c47c442..fc4aa1ae19eb 100644 --- a/spec/std/exception_spec.cr +++ b/spec/std/exception_spec.cr @@ -1,17 +1,5 @@ require "./spec_helper" -private def compile_and_run_file(source_file) - with_tempfile("executable_file") do |executable_file| - Process.run("bin/crystal", ["build", "--release", "-o", executable_file, source_file]) - File.exists?(executable_file).should be_true - - output, error = IO::Memory.new, IO::Memory.new - Process.run executable_file, output: output, error: error - - {output.to_s, error.to_s} - end -end - private class FooError < Exception def message "#{super || ""} -- bar!" @@ -51,7 +39,7 @@ describe "Exception" do it "collect memory within ensure block" do sample = datapath("collect_within_ensure") - output, error = compile_and_run_file(sample) + _, output, error = compile_and_run_file(sample, ["--release"]) output.to_s.empty?.should be_true error.to_s.should contain("Unhandled exception: Oh no! (Exception)") diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index 812891c66248..456121c15fed 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -359,7 +359,7 @@ describe "File" do File.chmod(path, 0o664) File.info(path).permissions.should eq(normalize_permissions(0o664, directory: true)) ensure - Dir.rmdir(path) if Dir.exists?(path) + Dir.delete(path) if Dir.exists?(path) end end @@ -1224,6 +1224,60 @@ describe "File" do end end + describe ".same_content?" do + it "compares two equal files" do + File.same_content?( + datapath("test_file.txt"), + datapath("test_file.txt") + ).should be_true + end + + it "compares two different files" do + File.same_content?( + datapath("test_file.txt"), + datapath("test_file.ini") + ).should be_false + end + end + + describe ".copy" do + it "copies a file" do + src_path = datapath("test_file.txt") + with_tempfile("cp.txt") do |out_path| + File.copy(src_path, out_path) + File.exists?(out_path).should be_true + File.same_content?(src_path, out_path).should be_true + end + end + + pending_win32 "copies permissions" do + with_tempfile("cp-permissions-src.txt", "cp-permissions-out.txt") do |src_path, out_path| + File.write(src_path, "foo") + File.chmod(src_path, 0o700) + + File.copy(src_path, out_path) + + File.info(out_path).permissions.should eq(File::Permissions.new(0o700)) + File.same_content?(src_path, out_path).should be_true + end + end + + pending_win32 "overwrites existing destination and permissions" do + with_tempfile("cp-permissions-src.txt", "cp-permissions-out.txt") do |src_path, out_path| + File.write(src_path, "foo") + File.chmod(src_path, 0o700) + + File.write(out_path, "bar") + File.chmod(out_path, 0o777) + + File.copy(src_path, out_path) + + File.info(out_path).permissions.should eq(File::Permissions.new(0o700)) + File.same_content?(src_path, out_path).should be_true + end + end + end + describe ".match?" do it "matches basics" do File.match?("abc", "abc").should be_true diff --git a/spec/std/file_utils_spec.cr b/spec/std/file_utils_spec.cr index e94444f71691..f3c2110bb937 100644 --- a/spec/std/file_utils_spec.cr +++ b/spec/std/file_utils_spec.cr @@ -1,27 +1,6 @@ require "./spec_helper" require "file_utils" -private class OneByOneIO < IO - @bytes : Bytes - - def initialize(string) - @bytes = string.to_slice - @pos = 0 - end - - def read(slice : Bytes) - return 0 if slice.empty? - return 0 if @pos >= @bytes.size - - slice[0] = @bytes[@pos] - @pos += 1 - 1 - end - - def write(slice : Bytes) : Nil - end -end - describe "FileUtils" do describe "cd" do it "should work" do @@ -69,30 +48,6 @@ describe "FileUtils" do datapath("test_file.ini") ).should be_false end - - it "compares two ios, one way (true)" do - io1 = OneByOneIO.new("hello") - io2 = IO::Memory.new("hello") - FileUtils.cmp(io1, io2).should be_true - end - - it "compares two ios, second way (true)" do - io1 = OneByOneIO.new("hello") - io2 = IO::Memory.new("hello") - FileUtils.cmp(io2, io1).should be_true - end - - it "compares two ios, one way (false)" do - io1 = OneByOneIO.new("hello") - io2 = IO::Memory.new("hella") - FileUtils.cmp(io1, io2).should be_false - end - - it "compares two ios, second way (false)" do - io1 = OneByOneIO.new("hello") - io2 = IO::Memory.new("hella") - FileUtils.cmp(io2, io1).should be_false - end end describe "touch" do @@ -170,6 +125,23 @@ describe "FileUtils" do File.exists?(File.join(dest_path, "b/c")).should be_true end end + + it "copies a directory recursively if destination exists leaving existing files" do + with_tempfile("cp_r-test", "cp_r-test-copied") do |src_path, dest_path| + Dir.mkdir_p(dest_path) + File.write(File.join(dest_path, "d"), "") + + Dir.mkdir_p(src_path) + File.write(File.join(src_path, "a"), "") + Dir.mkdir(File.join(src_path, "b")) + File.write(File.join(src_path, "b/c"), "") + + FileUtils.cp_r(src_path, dest_path) + File.exists?(File.join(dest_path, "a")).should be_true + File.exists?(File.join(dest_path, "b/c")).should be_true + File.exists?(File.join(dest_path, "d")).should be_true + end + end end describe "rm_r" do diff --git a/spec/std/flate/flate_spec.cr b/spec/std/flate/flate_spec.cr deleted file mode 100644 index f2071b571d76..000000000000 --- a/spec/std/flate/flate_spec.cr +++ /dev/null @@ -1,106 +0,0 @@ -require "spec" -require "flate" - -private def new_sample_io - io = IO::Memory.new - "cbc9cc4b350402ae1c20c30808b800".scan(/../).each do |match| - io.write_byte match[0].to_u8(16) - end - io.rewind -end - -module Flate - describe Reader do - it "should read byte by byte (#4192)" do - io = new_sample_io - - reader = Reader.new(io) - - str = String::Builder.build do |builder| - while b = reader.read_byte - builder.write_byte b - end - end - - str.should eq("line1111\nline2222\n") - end - - it "should rewind" do - io = new_sample_io - - reader = Reader.new(io) - reader.gets.should eq("line1111") - reader.rewind - reader.gets_to_end.should eq("line1111\nline2222\n") - end - - describe ".open" do - it "yields itself to block" do - # Hello Crystal! - message = Bytes[243, 72, 205, 201, 201, 87, 112, 46, 170, 44, 46, 73, - 204, 81, 4, 0] - - io = IO::Memory.new(message) - Reader.open(io) do |reader| - reader.gets_to_end.should eq("Hello Crystal!") - end - end - end - end - - describe Writer do - it "should be able to write" do - message = "this is a test string !!!!\n" - io = IO::Memory.new - writer = Writer.new(io) - writer.print message - writer.close - - io.rewind - reader = Reader.new(io) - reader.gets_to_end.should eq(message) - end - - it "can be closed without sync" do - io = IO::Memory.new - writer = Writer.new(io) - writer.close - writer.closed?.should be_true - io.closed?.should be_false - - expect_raises IO::Error, "Closed stream" do - writer.print "a" - end - end - - it "can be closed with sync (1)" do - io = IO::Memory.new - writer = Writer.new(io, sync_close: true) - writer.close - writer.closed?.should be_true - io.closed?.should be_true - end - - it "can be closed with sync (2)" do - io = IO::Memory.new - writer = Writer.new(io) - writer.sync_close = true - writer.close - writer.closed?.should be_true - io.closed?.should be_true - end - - describe ".open" do - it "yields itself to block" do - io = IO::Memory.new - Writer.open(io) do |writer| - writer.write "Hello Crystal!".to_slice - end - - io.rewind - io.to_slice.should eq(Bytes[243, 72, 205, 201, 201, 87, 112, 46, 170, 44, 46, 73, - 204, 81, 4, 0]) - end - end - end -end diff --git a/spec/std/gzip/gzip_spec.cr b/spec/std/gzip/gzip_spec.cr deleted file mode 100644 index 249855d8c899..000000000000 --- a/spec/std/gzip/gzip_spec.cr +++ /dev/null @@ -1,60 +0,0 @@ -require "spec" -require "gzip" - -private SAMPLE_TIME = Time.utc(2016, 1, 2) -private SAMPLE_OS = 4_u8 -private SAMPLE_EXTRA = Bytes[1, 2, 3] -private SAMPLE_NAME = "foo.txt" -private SAMPLE_COMMENT = "some comment" -private SAMPLE_CONTENTS = "hello world\nfoo bar" - -private def new_sample_io - io = IO::Memory.new - - Gzip::Writer.open(io) do |gzip| - header = gzip.header - header.modification_time = SAMPLE_TIME - header.os = SAMPLE_OS - header.extra = SAMPLE_EXTRA - header.name = SAMPLE_NAME - header.comment = SAMPLE_COMMENT - - io.bytesize.should eq(0) - gzip.flush - io.bytesize.should_not eq(0) - - gzip.print SAMPLE_CONTENTS - end - - io.rewind -end - -describe Gzip do - it "writes and reads to memory" do - io = new_sample_io - - Gzip::Reader.open(io) do |gzip| - header = gzip.header.not_nil! - header.modification_time.should eq(SAMPLE_TIME) - header.os.should eq(SAMPLE_OS) - header.extra.should eq(SAMPLE_EXTRA) - header.name.should eq(SAMPLE_NAME) - header.comment.should eq(SAMPLE_COMMENT) - - # Reading zero bytes is OK - gzip.read(Bytes.empty).should eq(0) - - gzip.gets_to_end.should eq(SAMPLE_CONTENTS) - end - end - - it "rewinds" do - io = new_sample_io - - gzip = Gzip::Reader.new(io) - gzip.gets.should eq(SAMPLE_CONTENTS.lines.first) - - gzip.rewind - gzip.gets_to_end.should eq(SAMPLE_CONTENTS) - end -end diff --git a/spec/std/hash_spec.cr b/spec/std/hash_spec.cr index e241f09404a0..35672eb5804d 100644 --- a/spec/std/hash_spec.cr +++ b/spec/std/hash_spec.cr @@ -14,6 +14,14 @@ end private alias RecursiveType = String | Int32 | Array(RecursiveType) | Hash(Symbol, RecursiveType) +private class HashWrapper(K, V) + include Enumerable({K, V}) + + @hash = {} of K => V + + delegate each, to: @hash +end + describe "Hash" do describe "empty" do it "size should be zero" do @@ -1225,4 +1233,8 @@ describe "Hash" do h.clone.compare_by_identity?.should be_true end end + + it "can be wrapped" do + HashWrapper(Int32, Int32).new.to_a.should be_empty + end end diff --git a/spec/std/http/client/client_spec.cr b/spec/std/http/client/client_spec.cr index 6b9453f47b32..4af7437bdc43 100644 --- a/spec/std/http/client/client_spec.cr +++ b/spec/std/http/client/client_spec.cr @@ -1,7 +1,9 @@ require "../spec_helper" +require "../../socket/spec_helper" require "openssl" require "http/client" require "http/server" +require "log/spec" private def test_server(host, port, read_time = 0, content_type = "text/plain", write_response = true) server = TCPServer.new(host, port) @@ -142,7 +144,7 @@ module HTTP end end - it "sends the host header ipv6 with brackets" do + pending_ipv6 "sends the host header ipv6 with brackets" do server = HTTP::Server.new do |context| context.response.print context.request.headers["Host"] end @@ -157,10 +159,10 @@ module HTTP server = HTTP::Server.new do |context| context.response.print context.request.headers["connection"] end - address = server.bind_unused_port "::1" + address = server.bind_unused_port "127.0.0.1" run_server(server) do - HTTP::Client.get("http://[::1]:#{address.port}/").body.should eq("close") + HTTP::Client.get("http://127.0.0.1:#{address.port}/").body.should eq("close") end end @@ -168,10 +170,10 @@ module HTTP server = HTTP::Server.new do |context| context.response.print context.request.headers["connection"] end - address = server.bind_unused_port "::1" + address = server.bind_unused_port "127.0.0.1" run_server(server) do - HTTP::Client.get("http://[::1]:#{address.port}/") do |response| + HTTP::Client.get("http://127.0.0.1:#{address.port}/") do |response| response.body_io.gets_to_end end.should eq("close") end @@ -223,19 +225,24 @@ module HTTP end end - it "tests write_timeout" do - # Here we don't want to write a response on the server side because - # it doesn't make sense to try to write because the client will already - # timeout on read. Writing a response could lead on an exception in - # the server if the socket is closed. - test_server("localhost", 0, 0, write_response: false) do |server| - client = Client.new("localhost", server.local_address.port) - expect_raises(IO::TimeoutError, "Write timed out") do - client.write_timeout = 0.001 - client.post("/", body: "a" * 5_000_000) + {% unless flag?(:darwin) %} + # TODO the following spec is failing on Nix Darwin CI when executed + # together with some other tests. If run alone it succeeds. + # The exhibit failure is a Failed to raise an exception: END_OF_STACK. + it "tests write_timeout" do + # Here we don't want to write a response on the server side because + # it doesn't make sense to try to write because the client will already + # timeout on read. Writing a response could lead on an exception in + # the server if the socket is closed. + test_server("localhost", 0, 0, write_response: false) do |server| + client = Client.new("localhost", server.local_address.port) + expect_raises(IO::TimeoutError, "Write timed out") do + client.write_timeout = 0.001 + client.post("/", body: "a" * 5_000_000) + end end end - end + {% end %} it "tests connect_timeout" do test_server("localhost", 0, 0) do |server| @@ -264,5 +271,73 @@ module HTTP request.host.should eq "other.example.com" end end + + it "works with IO" do + io_response = IO::Memory.new <<-RESPONSE.gsub('\n', "\r\n") + HTTP/1.1 200 OK + Content-Type: text/plain + Content-Length: 3 + + Hi! + RESPONSE + io_request = IO::Memory.new + io = IO::Stapled.new(io_response, io_request) + client = Client.new(io) + response = client.get("/") + response.body.should eq("Hi!") + + io_request.rewind + request = HTTP::Request.from_io(io_request).as(HTTP::Request) + request.host.should eq("") + end + + it "can specify host and port when initialized with IO" do + client = Client.new(IO::Memory.new, "host", 1234) + client.host.should eq("host") + client.port.should eq(1234) + end + + it "cannot reconnect when initialized with IO" do + io = IO::Memory.new + client = Client.new(io) + client.close + io.closed?.should be_true + expect_raises(Exception, "This HTTP::Client cannot be reconnected") do + client.get("/") + end + end + + describe "logging" do + it "emit logs" do + test_server("localhost", 0, content_type: "") do |server| + client = Client.new("localhost", server.local_address.port) + Log.capture("http.client") do |logs| + client.get("/") + + logs.check(:debug, "Performing request") + logs.entry.data[:method].should eq("GET") + logs.entry.data[:host].should eq("localhost") + logs.entry.data[:port].should eq(server.local_address.port) + logs.entry.data[:resource].should eq("/") + end + end + end + + it "emit logs with block" do + test_server("localhost", 0, content_type: "") do |server| + Client.new("localhost", server.local_address.port) do |client| + Log.capture("http.client") do |logs| + client.get("/") do |response| + logs.check(:debug, "Performing request") + logs.entry.data[:method].should eq("GET") + logs.entry.data[:host].should eq("localhost") + logs.entry.data[:port].should eq(server.local_address.port) + logs.entry.data[:resource].should eq("/") + end + end + end + end + end + end end end diff --git a/spec/std/http/params_spec.cr b/spec/std/http/params_spec.cr index 631dac4160c2..0eb1fe027ea4 100644 --- a/spec/std/http/params_spec.cr +++ b/spec/std/http/params_spec.cr @@ -116,10 +116,10 @@ module HTTP end describe "#[]=(name, value)" do - it "sets first value for provided param name" do + it "sets value for provided param name" do params = Params.parse("foo=bar&foo=baz&baz=qux") params["foo"] = "notfoo" - params.fetch_all("foo").should eq(["notfoo", "baz"]) + params.fetch_all("foo").should eq(["notfoo"]) end it "adds new name => value pair if there is no such param" do @@ -127,6 +127,12 @@ module HTTP params["non_existent_param"] = "test" params.fetch_all("non_existent_param").should eq(["test"]) end + + it "sets value for provided param name (array)" do + params = Params.parse("foo=bar&foo=baz&baz=qux") + params["non_existent_param"] = ["test", "something"] + params.fetch_all("non_existent_param").should eq(["test", "something"]) + end end describe "#fetch(name, default)" do @@ -247,5 +253,21 @@ module HTTP Params.new.empty?.should be_true end end + + describe "#==" do + it "compares other" do + a = Params.parse("a=foo&b=bar") + b = Params.parse("a=bar&b=foo") + (a == a).should be_true + (b == b).should be_true + (a == b).should be_false + end + + it "compares other types" do + a = Params.parse("a=foo&b=bar") + b = "other type" + (a == b).should be_false + end + end end end diff --git a/spec/std/http/request_spec.cr b/spec/std/http/request_spec.cr index e06a4c193dc4..536cb203ff92 100644 --- a/spec/std/http/request_spec.cr +++ b/spec/std/http/request_spec.cr @@ -408,7 +408,7 @@ module HTTP params = request.query_params params["foo"] = "not-bar" - request.query.should eq("foo=not-bar&foo=baz&baz=qux") + request.query.should eq("foo=not-bar&baz=qux") end it "updates @resource when modified" do @@ -416,7 +416,7 @@ module HTTP params = request.query_params params["foo"] = "not-bar" - request.resource.should eq("/api/v3/some/resource?foo=not-bar&foo=baz&baz=qux") + request.resource.should eq("/api/v3/some/resource?foo=not-bar&baz=qux") end it "updates serialized form when modified" do @@ -427,7 +427,7 @@ module HTTP io = IO::Memory.new request.to_io(io) - io.to_s.should eq("GET /api/v3/some/resource?foo=not-bar&foo=baz&baz=qux HTTP/1.1\r\n\r\n") + io.to_s.should eq("GET /api/v3/some/resource?foo=not-bar&baz=qux HTTP/1.1\r\n\r\n") end it "is affected when #query is modified" do diff --git a/spec/std/http/server/handlers/compress_handler_spec.cr b/spec/std/http/server/handlers/compress_handler_spec.cr index 7d08e330538b..f6c0b5ab653d 100644 --- a/spec/std/http/server/handlers/compress_handler_spec.cr +++ b/spec/std/http/server/handlers/compress_handler_spec.cr @@ -66,4 +66,88 @@ describe HTTP::CompressHandler do gzip = Compress::Gzip::Reader.new(io2) gzip.gets_to_end.should eq("Hello") end + + it "doesn't compress twice" do + io = IO::Memory.new + request = HTTP::Request.new("GET", "/") + request.headers["Accept-Encoding"] = "gzip" + + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + + handler1 = HTTP::CompressHandler.new + handler2 = HTTP::CompressHandler.new + handler1.next = handler2 + handler2.next = HTTP::Handler::HandlerProc.new do |ctx| + ctx.response.print "Hello" + end + handler1.call(context) + response.close + + io.rewind + response2 = HTTP::Client::Response.from_io(io) + response2.body.should eq("Hello") + end + + it "fix content-length header" do + io = IO::Memory.new + request = HTTP::Request.new("GET", "/") + request.headers["Accept-Encoding"] = "gzip" + + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + + handler = HTTP::CompressHandler.new + handler.next = HTTP::Handler::HandlerProc.new do |ctx| + ctx.response.content_length = 5 + ctx.response.print "Hello" + ctx.response.flush + end + handler.call(context) + response.close + + io.rewind + response = HTTP::Client::Response.from_io(io) + response.body.should eq("Hello") + end + + it "don't try to compress for empty body responses" do + io = IO::Memory.new + request = HTTP::Request.new("GET", "/") + request.headers["Accept-Encoding"] = "gzip" + + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + + handler = HTTP::CompressHandler.new + handler.next = HTTP::Handler::HandlerProc.new do |ctx| + context.response.status = :not_modified + end + handler.call(context) + response.close + + io.rewind + io.to_s.should eq("HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\n\r\n") + end + + it "don't try to compress upgraded response" do + io = IO::Memory.new + request = HTTP::Request.new("GET", "/") + request.headers["Accept-Encoding"] = "gzip" + + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + + handler = HTTP::CompressHandler.new + handler.next = HTTP::Handler::HandlerProc.new do |ctx| + response.status = :switching_protocols + response.upgrade do |io| + end + end + handler.call(context) + response.close + + io.rewind + io.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\n\r\n") + end end diff --git a/spec/std/http/server/handlers/log_handler_spec.cr b/spec/std/http/server/handlers/log_handler_spec.cr index e7d7e2de046a..c2a15c385626 100644 --- a/spec/std/http/server/handlers/log_handler_spec.cr +++ b/spec/std/http/server/handlers/log_handler_spec.cr @@ -2,22 +2,27 @@ require "spec" require "log/spec" require "http/server/handler" require "../../../../support/io" +require "../../../../support/retry" describe HTTP::LogHandler do - it "logs" do - io = IO::Memory.new - request = HTTP::Request.new("GET", "/") - request.remote_address = "192.168.0.1" - response = HTTP::Server::Response.new(io) - context = HTTP::Server::Context.new(request, response) + {% unless flag?(:win32) %} + # TODO: Remove this once `Socket` is working on Windows - called = false - handler = HTTP::LogHandler.new - handler.next = ->(ctx : HTTP::Server::Context) { called = true } - logs = Log.capture("http.server") { handler.call(context) } - logs.check(:info, %r(^192.168.0.1 - GET / HTTP/1.1 - 200 \(\d+(\.\d+)?[mµn]s\)$)) - called.should be_true - end + it "logs" do + io = IO::Memory.new + request = HTTP::Request.new("GET", "/") + request.remote_address = Socket::IPAddress.new("192.168.0.1", 1234) + response = HTTP::Server::Response.new(io) + context = HTTP::Server::Context.new(request, response) + + called = false + handler = HTTP::LogHandler.new + handler.next = ->(ctx : HTTP::Server::Context) { called = true } + logs = Log.capture("http.server") { handler.call(context) } + logs.check(:info, %r(^192.168.0.1 - GET / HTTP/1.1 - 200 \(\d+(\.\d+)?[mµn]s\)$)) + called.should be_true + end + {% end %} it "logs to custom logger" do request = HTTP::Request.new("GET", "/") @@ -45,7 +50,9 @@ describe HTTP::LogHandler do handler.next = ->(ctx : HTTP::Server::Context) {} handler.call(context) - io.to_s.should match(%r(- - GET / HTTP/1.1 - 200 \(\d+(\.\d+)?[mµn]s\)$)) + retry do + io.to_s.should match(%r(- - GET / HTTP/1.1 - 200 \(\d+(\.\d+)?[mµn]s\)$)) + end end it "log failed request" do diff --git a/spec/std/http/server/handlers/static_file_handler_spec.cr b/spec/std/http/server/handlers/static_file_handler_spec.cr index 8ed0fca44ca4..60603f75427c 100644 --- a/spec/std/http/server/handlers/static_file_handler_spec.cr +++ b/spec/std/http/server/handlers/static_file_handler_spec.cr @@ -250,4 +250,31 @@ describe HTTP::StaticFileHandler do response = handle HTTP::Request.new("GET", "/test.txt%0A") response.status_code.should eq(404) end + + it "serve compressed content" do + modification_time = File.info(datapath("static_file_handler", "test.txt")).modification_time + File.touch datapath("static_file_handler", "test.txt.gz"), modification_time + 1.second + + headers = HTTP::Headers{"Accept-Encoding" => "gzip"} + response = handle HTTP::Request.new("GET", "/test.txt", headers) + response.headers["Content-Encoding"].should eq("gzip") + end + + it "still serve compressed content when modification time is very close" do + modification_time = File.info(datapath("static_file_handler", "test.txt")).modification_time + File.touch datapath("static_file_handler", "test.txt.gz"), modification_time - 1.microsecond + + headers = HTTP::Headers{"Accept-Encoding" => "gzip"} + response = handle HTTP::Request.new("GET", "/test.txt", headers) + response.headers["Content-Encoding"].should eq("gzip") + end + + it "doesn't serve compressed content if older than raw file" do + modification_time = File.info(datapath("static_file_handler", "test.txt")).modification_time + File.touch datapath("static_file_handler", "test.txt.gz"), modification_time - 1.second + + headers = HTTP::Headers{"Accept-Encoding" => "gzip"} + response = handle HTTP::Request.new("GET", "/test.txt", headers) + response.headers["Content-Encoding"]?.should be_nil + end end diff --git a/spec/std/http/server/request_processor_spec.cr b/spec/std/http/server/request_processor_spec.cr index 72c2f6c3f4a2..089ef70525e0 100644 --- a/spec/std/http/server/request_processor_spec.cr +++ b/spec/std/http/server/request_processor_spec.cr @@ -268,9 +268,9 @@ describe HTTP::Server::RequestProcessor do logs.entry.exception.should be_a(IO::Error) end - it "catches raised error on handler" do + it "catches raised error on handler and retains context from handler" do exception = Exception.new "OH NO" - processor = HTTP::Server::RequestProcessor.new { raise exception } + processor = HTTP::Server::RequestProcessor.new { Log.context.set foo: "bar"; raise exception } input = IO::Memory.new("GET / HTTP/1.1\r\n\r\n") output = IO::Memory.new logs = Log.capture("http.server") do @@ -286,6 +286,7 @@ describe HTTP::Server::RequestProcessor do logs.check(:error, "Unhandled exception on HTTP::Handler") logs.entry.exception.should eq(exception) + logs.entry.context[:foo].should eq "bar" end it "doesn't respond with error when headers were already sent" do @@ -320,4 +321,32 @@ describe HTTP::Server::RequestProcessor do client_response.status_code.should eq(200) client_response.body.should eq("Hello world") end + + it "does not bleed Log::Context between requests" do + processor = HTTP::Server::RequestProcessor.new do |context| + Log.info { "before" } + Log.context.set foo: "bar" + Log.info { "after" } + + context.response.content_type = "text/plain" + context.response.print "Hello world" + end + + logs = Log.capture do + processor.process( + IO::Memory.new("GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\n\r\n"), + IO::Memory.new, + ) + end + + logs.check :info, "before" + logs.entry.context.should be_empty + logs.check :info, "after" + logs.entry.context[:foo].should eq "bar" + + logs.check :info, "before" + logs.entry.context.should be_empty + logs.check :info, "after" + logs.entry.context[:foo].should eq "bar" + end end diff --git a/spec/std/http/server/response_spec.cr b/spec/std/http/server/response_spec.cr index 33bd0aa70ada..0042f40c008f 100644 --- a/spec/std/http/server/response_spec.cr +++ b/spec/std/http/server/response_spec.cr @@ -91,6 +91,14 @@ describe HTTP::Server::Response do io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n1234567890") end + it "doesn't override content-length when there's no body" do + io = IO::Memory.new + response = Response.new(io) + response.content_length = 10 + response.close + io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n") + end + it "adds header" do io = IO::Memory.new response = Response.new(io) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index 84decd23afb4..6c39a8e21aff 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -493,7 +493,7 @@ describe "#remote_address" do HTTP::Client.new(URI.parse("http://#{address1}/")) do |client| client.get("/") - remote_address.should eq(client.@socket.as(IPSocket).local_address.to_s) + remote_address.should eq(client.@io.as(IPSocket).local_address) end end end @@ -516,7 +516,7 @@ describe "#remote_address" do uri: URI.parse("https://#{ip_address1}"), tls: client_context) do |client| client.get("/") - remote_address.should eq(client.@socket.as(OpenSSL::SSL::Socket).local_address.to_s) + remote_address.should eq(client.@io.as(OpenSSL::SSL::Socket).local_address) end end end diff --git a/spec/std/http/spec_helper.cr b/spec/std/http/spec_helper.cr index 8fe39b8dcd07..e3c23a5ff448 100644 --- a/spec/std/http/spec_helper.cr +++ b/spec/std/http/spec_helper.cr @@ -1,5 +1,6 @@ require "spec" require "../spec_helper" +require "../../support/fibers" private def wait_for(timeout = 5.seconds) now = Time.monotonic @@ -25,7 +26,7 @@ end def run_server(server) server_done = Channel(Exception?).new - spawn do + f = spawn do server.listen rescue exc server_done.send exc @@ -35,6 +36,7 @@ def run_server(server) begin wait_for { server.listening? } + wait_until_blocked f yield server_done ensure diff --git a/spec/std/http/web_socket_spec.cr b/spec/std/http/web_socket_spec.cr index 9d940bb9d4ea..c6ec2c4271e0 100644 --- a/spec/std/http/web_socket_spec.cr +++ b/spec/std/http/web_socket_spec.cr @@ -28,6 +28,15 @@ private def assert_packet(packet, opcode, size, final = false) packet.final.should eq(final) end +private class MalformerHandler + include HTTP::Handler + + def call(context) + context.response.headers["Transfer-Encoding"] = "chunked" + call_next(context) + end +end + describe HTTP::WebSocket do describe "receive" do it "can read a small text packet" do @@ -384,6 +393,7 @@ describe HTTP::WebSocket do ws.on_message do |str| ws.send("pong #{str}") + ws.close end ws.on_close do @@ -406,7 +416,6 @@ describe HTTP::WebSocket do random = Random::Secure.hex ws2.on_message do |str| str.should eq("pong #{random}") - ws2.close end ws2.send(random) @@ -428,6 +437,54 @@ describe HTTP::WebSocket do end end + it "ignores body in upgrade response (malformed)" do + malformer = MalformerHandler.new + ws_handler = HTTP::WebSocketHandler.new do |ws, ctx| + ws.on_message do |str| + ws.send(str) + end + end + http_server = HTTP::Server.new([malformer, ws_handler]) + + address = http_server.bind_unused_port + + run_server(http_server) do + client = HTTP::WebSocket.new("ws://#{address}") + message = nil + client.on_message do |msg| + message = msg + client.close + end + client.send "hello" + client.run + message.should eq("hello") + end + end + + it "doesn't compress upgrade response body" do + compress_handler = HTTP::CompressHandler.new + ws_handler = HTTP::WebSocketHandler.new do |ws, ctx| + ws.on_message do |str| + ws.send(str) + end + end + http_server = HTTP::Server.new([compress_handler, ws_handler]) + + address = http_server.bind_unused_port + + run_server(http_server) do + client = HTTP::WebSocket.new("ws://#{address}", headers: HTTP::Headers{"Accept-Encoding" => "gzip"}) + message = nil + client.on_message do |msg| + message = msg + client.close + end + client.send "hello" + client.run + message.should eq("hello") + end + end + describe "handshake fails if server does not verify Sec-WebSocket-Key" do it "Sec-WebSocket-Accept missing" do http_server = HTTP::Server.new do |context| diff --git a/spec/std/int_spec.cr b/spec/std/int_spec.cr index 8af61717f197..bccae5a8292d 100644 --- a/spec/std/int_spec.cr +++ b/spec/std/int_spec.cr @@ -4,11 +4,13 @@ require "./spec_helper" {% end %} private def to_s_with_io(num) - String.build { |str| num.to_s(str) } + String.build { |io| num.to_s(io) } end private def to_s_with_io(num, base, upcase = false) - String.build { |str| num.to_s(base, str, upcase) } + String.build { |io| num.to_s(io, base, upcase: upcase) } + # Test deprecated overload: + String.build { |io| num.to_s(base, io, upcase) } end describe "Int" do @@ -172,6 +174,7 @@ describe "Int" do it { 1234.to_s(36).should eq("ya") } it { -1234.to_s(36).should eq("-ya") } it { 1234.to_s(16, upcase: true).should eq("4D2") } + it { 1234.to_s(16, true).should eq("4D2") } # Deprecated test it { -1234.to_s(16, upcase: true).should eq("-4D2") } it { 1234.to_s(36, upcase: true).should eq("YA") } it { -1234.to_s(36, upcase: true).should eq("-YA") } @@ -796,4 +799,43 @@ describe "Int" do (10.to_big_i ** 3010).bit_length.should eq(10000) end end + + describe "#digits" do + it "works for positive numbers or zero" do + 0.digits.should eq([0]) + 1.digits.should eq([1]) + 10.digits.should eq([0, 1]) + 123.digits.should eq([3, 2, 1]) + 123456789.digits.should eq([9, 8, 7, 6, 5, 4, 3, 2, 1]) + end + + it "works for maximums" do + Int32::MAX.digits.should eq(Int32::MAX.to_s.chars.map(&.to_i).reverse) + Int64::MAX.digits.should eq(Int64::MAX.to_s.chars.map(&.to_i).reverse) + UInt64::MAX.digits.should eq(UInt64::MAX.to_s.chars.map(&.to_i).reverse) + end + + it "works for non-Int32" do + digits = 123_i64.digits + digits.should eq([3, 2, 1]) + end + + it "works with a base" do + 123.digits(16).should eq([11, 7]) + end + + it "raises for invalid base" do + [1, 0, -1].each do |base| + expect_raises(ArgumentError, "Invalid base #{base}") do + 123.digits(base) + end + end + end + + it "raises for negative numbers" do + expect_raises(ArgumentError, "Can't request digits of negative number") do + -123.digits + end + end + end end diff --git a/spec/std/io/file_descriptor_spec.cr b/spec/std/io/file_descriptor_spec.cr index 37ff29a5ed60..b1ce0c33cdfd 100644 --- a/spec/std/io/file_descriptor_spec.cr +++ b/spec/std/io/file_descriptor_spec.cr @@ -1,11 +1,11 @@ -require "../../spec_helper" +require "../spec_helper" describe IO::FileDescriptor do - it "reopen STDIN with the right mode" do + pending_win32 "reopen STDIN with the right mode" do code = %q(puts "#{STDIN.blocking} #{STDIN.info.type}") - build(code) do |binpath| - `#{binpath} < #{binpath}`.chomp.should eq("true File") - `echo "" | #{binpath}`.chomp.should eq("false Pipe") + compile_source(code) do |binpath| + `#{Process.quote(binpath)} < #{Process.quote(binpath)}`.chomp.should eq("true File") + `echo "" | #{Process.quote(binpath)}`.chomp.should eq("false Pipe") end end end diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr index f5cde6ed1f51..b740b114789a 100644 --- a/spec/std/io/io_spec.cr +++ b/spec/std/io/io_spec.cr @@ -2,6 +2,7 @@ require "../spec_helper" require "../../support/channel" {% unless flag?(:win32) %} + require "socket" require "big" {% end %} require "base64" @@ -55,8 +56,6 @@ private class SimpleIOMemory < IO slice.copy_to(@buffer + @bytesize, count) @bytesize += count - - nil end def to_slice @@ -82,6 +81,27 @@ private class SimpleIOMemory < IO end end +private class OneByOneIO < IO + @bytes : Bytes + + def initialize(string) + @bytes = string.to_slice + @pos = 0 + end + + def read(slice : Bytes) + return 0 if slice.empty? + return 0 if @pos >= @bytes.size + + slice[0] = @bytes[@pos] + @pos += 1 + 1 + end + + def write(slice : Bytes) : Nil + end +end + describe IO do describe "partial read" do pending_win32 "doesn't block on first read. blocks on 2nd read" do @@ -398,6 +418,32 @@ describe IO do end end end + + describe ".same_content?" do + it "compares two ios, one way (true)" do + io1 = OneByOneIO.new("hello") + io2 = IO::Memory.new("hello") + IO.same_content?(io1, io2).should be_true + end + + it "compares two ios, second way (true)" do + io1 = OneByOneIO.new("hello") + io2 = IO::Memory.new("hello") + IO.same_content?(io2, io1).should be_true + end + + it "compares two ios, one way (false)" do + io1 = OneByOneIO.new("hello") + io2 = IO::Memory.new("hella") + IO.same_content?(io1, io2).should be_false + end + + it "compares two ios, second way (false)" do + io1 = OneByOneIO.new("hello") + io2 = IO::Memory.new("hella") + IO.same_content?(io2, io1).should be_false + end + end end describe "write operations" do @@ -722,6 +768,24 @@ describe IO do io.read_string(11).should eq("Hello world") io.gets_to_end.should eq("\r\nFoo\nBar") end + + pending_win32 "gets ascii from socket (#9056)" do + server = TCPServer.new "localhost", 0 + sock = TCPSocket.new "localhost", server.local_address.port + begin + sock.set_encoding("ascii") + spawn do + client = server.accept + message = client.gets + client << "#{message}\n" + end + sock << "K\n" + sock.gets.should eq("K") + ensure + server.close + sock.close + end + end end describe "encode" do diff --git a/spec/std/io/stapled_spec.cr b/spec/std/io/stapled_spec.cr index ef27598d9d57..6c6a315119f7 100644 --- a/spec/std/io/stapled_spec.cr +++ b/spec/std/io/stapled_spec.cr @@ -76,6 +76,22 @@ describe IO::Stapled do io.peek.should eq Bytes.empty end + it "#skip delegates to reader" do + reader = IO::Memory.new "cletus" + io = IO::Stapled.new reader, IO::Memory.new + io.peek.should eq "cletus".to_slice + io.skip(4) + io.peek.should eq "us".to_slice + end + + it "#skip_to_end delegates to reader" do + reader = IO::Memory.new "cletus" + io = IO::Stapled.new reader, IO::Memory.new + io.peek.should eq "cletus".to_slice + io.skip_to_end + io.peek.should eq Bytes.empty + end + describe ".pipe" do it "creates a bidirectional pipe" do a, b = IO::Stapled.pipe diff --git a/spec/std/iterator_spec.cr b/spec/std/iterator_spec.cr index 5aa4a9e0e899..4cb73cd91e51 100644 --- a/spec/std/iterator_spec.cr +++ b/spec/std/iterator_spec.cr @@ -177,6 +177,10 @@ describe Iterator do iter.next.should eq({4, 5}) iter.next.should be_a(Iterator::Stop) end + + it "doesn't include stop in return type" do + (1..3).each.cons_pair.to_a.should eq([{1, 2}, {2, 3}]) + end end describe "cycle" do diff --git a/spec/std/json/any_spec.cr b/spec/std/json/any_spec.cr index 9178f44a5403..9758bbad0022 100644 --- a/spec/std/json/any_spec.cr +++ b/spec/std/json/any_spec.cr @@ -1,8 +1,6 @@ require "../spec_helper" require "json" -{% unless flag?(:win32) %} - require "yaml" -{% end %} +require "yaml" describe JSON::Any do describe "casts" do @@ -172,7 +170,7 @@ describe JSON::Any do any2.as_a[0].as_a.should_not be(any.as_a[0].as_a) end - pending_win32 "#to_yaml" do + it "#to_yaml" do any = JSON.parse <<-JSON { "foo": "bar", diff --git a/spec/std/json/mapping_spec.cr b/spec/std/json/mapping_spec.cr deleted file mode 100644 index d03c9fa3b127..000000000000 --- a/spec/std/json/mapping_spec.cr +++ /dev/null @@ -1,640 +0,0 @@ -require "../spec_helper" -require "json" -require "uuid" -require "uuid/json" -{% unless flag?(:win32) %} - require "big/json" -{% end %} - -private class JSONPerson - JSON.mapping({ - name: {type: String}, - age: {type: Int32, nilable: true}, - }) - - def_equals name, age - - def initialize(@name : String) - end -end - -private class StrictJSONPerson - JSON.mapping({ - name: {type: String}, - age: {type: Int32, nilable: true}, - }, true) -end - -private class JSONPersonEmittingNull - JSON.mapping({ - name: {type: String}, - age: {type: Int32, nilable: true, emit_null: true}, - }) -end - -private class JSONWithBool - JSON.mapping value: Bool -end - -private class JSONWithUUID - JSON.mapping value: UUID -end - -{% unless flag?(:win32) %} - private class JSONWithBigDecimal - JSON.mapping value: BigDecimal - end -{% end %} - -private class JSONWithTime - JSON.mapping({ - value: {type: Time, converter: Time::Format.new("%F %T")}, - }) -end - -private class JSONWithNilableTime - JSON.mapping({ - value: {type: Time, nilable: true, converter: Time::Format.new("%F")}, - }) - - def initialize - end -end - -private class JSONWithNilableTimeEmittingNull - JSON.mapping({ - value: {type: Time, nilable: true, converter: Time::Format.new("%F"), emit_null: true}, - }) - - def initialize - end -end - -private class JSONWithPropertiesKey - JSON.mapping( - properties: Hash(String, String), - ) -end - -private class JSONWithSimpleMapping - JSON.mapping({name: String, age: Int32}) -end - -private class JSONWithKeywordsMapping - JSON.mapping({end: Int32, abstract: Int32}) -end - -private class JSONWithAny - JSON.mapping({name: String, any: JSON::Any}) -end - -private class JsonWithProblematicKeys - JSON.mapping({ - key: Int32, - pull: Int32, - }) -end - -private class JsonWithSet - JSON.mapping({set: Set(String)}) -end - -private class JsonWithDefaults - JSON.mapping({ - a: {type: Int32, default: 11}, - b: {type: String, default: "Haha"}, - c: {type: Bool, default: true}, - d: {type: Bool, default: false}, - e: {type: Bool, nilable: true, default: false}, - f: {type: Int32, nilable: true, default: 1}, - g: {type: Int32, nilable: true, default: nil}, - h: {type: Array(Int32), default: [1, 2, 3]}, - }) -end - -private class JSONWithSmallIntegers - JSON.mapping({ - foo: Int16, - bar: Int8, - }) -end - -private class JSONWithTimeEpoch - JSON.mapping({ - value: {type: Time, converter: Time::EpochConverter}, - }) -end - -private class JSONWithTimeEpochMillis - JSON.mapping({ - value: {type: Time, converter: Time::EpochMillisConverter}, - }) -end - -private class JSONWithRaw - JSON.mapping({ - value: {type: String, converter: String::RawConverter}, - }) -end - -private class JSONWithArrayConverter - JSON.mapping({ - values: {type: Array(Time), converter: JSON::ArrayConverter(Time::EpochConverter)}, - }) -end - -private class JSONWithJSONHashValueConverter - JSON.mapping({ - birthdays: {type: Hash(String, Time), converter: JSON::HashValueConverter(Time::EpochConverter)}, - }) -end - -private class JSONWithRoot - JSON.mapping({ - result: {type: Array(JSONPerson), root: "heroes"}, - }) -end - -private class JSONWithNilableRoot - JSON.mapping({ - result: {type: Array(JSONPerson), root: "heroes", nilable: true}, - }) -end - -private class JSONWithNilableRootEmitNull - JSON.mapping({ - result: {type: Array(JSONPerson), root: "heroes", nilable: true, emit_null: true}, - }) -end - -private class JSONWithNilableUnion - JSON.mapping({ - value: Int32 | Nil, - }) -end - -private class JSONWithNilableUnion2 - JSON.mapping({ - value: Int32?, - }) -end - -private class JSONWithPresence - JSON.mapping({ - first_name: {type: String?, presence: true, nilable: true}, - last_name: {type: String?, presence: true, nilable: true}, - }) -end - -private class JSONWithQueryAttributes - JSON.mapping({ - foo?: Bool, - bar?: {type: Bool, default: false, presence: true, key: "is_bar"}, - }) -end - -private class JSONWithOverwritingQueryAttributes - property foo : Symbol? - property bar : Symbol? - JSON.mapping({ - foo?: Bool, - bar?: {type: Bool, default: false, presence: true, key: "is_bar"}, - }) -end - -describe "JSON mapping" do - it "parses person" do - person = JSONPerson.from_json(%({"name": "John", "age": 30})) - person.should be_a(JSONPerson) - person.name.should eq("John") - person.age.should eq(30) - end - - it "parses person without age" do - person = JSONPerson.from_json(%({"name": "John"})) - person.should be_a(JSONPerson) - person.name.should eq("John") - person.name.size.should eq(4) # This verifies that name is not nilable - person.age.should be_nil - end - - it "parses array of people" do - people = Array(JSONPerson).from_json(%([{"name": "John"}, {"name": "Doe"}])) - people.size.should eq(2) - end - - it "does to_json" do - person = JSONPerson.from_json(%({"name": "John", "age": 30})) - person2 = JSONPerson.from_json(person.to_json) - person2.should eq(person) - end - - it "parses person with unknown attributes" do - person = JSONPerson.from_json(%({"name": "John", "age": 30, "foo": "bar"})) - person.should be_a(JSONPerson) - person.name.should eq("John") - person.age.should eq(30) - end - - it "parses strict person with unknown attributes" do - error_message = <<-'MSG' - Unknown JSON attribute: foo - parsing StrictJSONPerson - MSG - ex = expect_raises JSON::MappingError, error_message do - StrictJSONPerson.from_json <<-JSON - { - "name": "John", - "age": 30, - "foo": "bar" - } - JSON - end - ex.location.should eq({4, 3}) - end - - it "raises if non-nilable attribute is nil" do - error_message = <<-'MSG' - Missing JSON attribute: name - parsing JSONPerson at 1:1 - MSG - ex = expect_raises JSON::MappingError, error_message do - JSONPerson.from_json(%({"age": 30})) - end - ex.location.should eq({1, 1}) - end - - it "raises if not an object" do - error_message = <<-'MSG' - Expected BeginObject but was String at 1:1 - parsing StrictJSONPerson at 0:0 - MSG - ex = expect_raises JSON::MappingError, error_message do - StrictJSONPerson.from_json <<-JSON - "foo" - JSON - end - ex.location.should eq({1, 1}) - end - - it "raises if data type does not match" do - error_message = <<-'MSG' - Expected Int but was String at 3:15 - parsing StrictJSONPerson#age at 3:3 - MSG - ex = expect_raises JSON::MappingError, error_message do - StrictJSONPerson.from_json <<-JSON - { - "name": "John", - "age": "foo", - "foo": "bar" - } - JSON - end - ex.location.should eq({3, 15}) - end - - it "doesn't emit null by default when doing to_json" do - person = JSONPerson.from_json(%({"name": "John"})) - (person.to_json =~ /age/).should be_falsey - end - - it "emits null on request when doing to_json" do - person = JSONPersonEmittingNull.from_json(%({"name": "John"})) - (person.to_json =~ /age/).should be_truthy - end - - it "doesn't raises on false value when not-nil" do - json = JSONWithBool.from_json(%({"value": false})) - json.value.should be_false - end - - it "parses UUID" do - uuid = JSONWithUUID.from_json(%({"value": "ba714f86-cac6-42c7-8956-bcf5105e1b81"})) - uuid.should be_a(JSONWithUUID) - uuid.value.should eq(UUID.new("ba714f86-cac6-42c7-8956-bcf5105e1b81")) - end - - it "parses json with Time::Format converter" do - json = JSONWithTime.from_json(%({"value": "2014-10-31 23:37:16"})) - json.value.should be_a(Time) - json.value.to_s.should eq("2014-10-31 23:37:16 UTC") - json.to_json.should eq(%({"value":"2014-10-31 23:37:16"})) - end - - it "allows setting a nilable property to nil" do - person = JSONPerson.new("John") - person.age = 1 - person.age = nil - end - - it "parses simple mapping" do - person = JSONWithSimpleMapping.from_json(%({"name": "John", "age": 30})) - person.should be_a(JSONWithSimpleMapping) - person.name.should eq("John") - person.age.should eq(30) - end - - it "outputs with converter when nilable" do - json = JSONWithNilableTime.new - json.to_json.should eq("{}") - end - - it "outputs with converter when nilable when emit_null is true" do - json = JSONWithNilableTimeEmittingNull.new - json.to_json.should eq(%({"value":null})) - end - - it "outputs JSON with properties key" do - string = %({"properties":{"foo":"bar"}}) - json = JSONWithPropertiesKey.from_json(string) - json.to_json.should eq(string) - end - - it "parses json with keywords" do - json = JSONWithKeywordsMapping.from_json(%({"end": 1, "abstract": 2})) - json.end.should eq(1) - json.abstract.should eq(2) - end - - it "parses json with any" do - json = JSONWithAny.from_json(%({"name": "Hi", "any": [{"x": 1}, 2, "hey", true, false, 1.5, null]})) - json.name.should eq("Hi") - json.any.raw.should eq([{"x" => 1}, 2, "hey", true, false, 1.5, nil]) - json.to_json.should eq(%({"name":"Hi","any":[{"x":1},2,"hey",true,false,1.5,null]})) - end - - it "parses json with problematic keys" do - json = JsonWithProblematicKeys.from_json(%({"key": 1, "pull": 2})) - json.key.should eq(1) - json.pull.should eq(2) - end - - it "parses json array as set" do - json = JsonWithSet.from_json(%({"set": ["a", "a", "b"]})) - json.set.should eq(Set(String){"a", "b"}) - end - - it "allows small types of integer" do - json = JSONWithSmallIntegers.from_json(%({"foo": 23, "bar": 7})) - - json.foo.should eq(23) - typeof(json.foo).should eq(Int16) - - json.bar.should eq(7) - typeof(json.bar).should eq(Int8) - end - - describe "parses json with defaults" do - it "mixed" do - json = JsonWithDefaults.from_json(%({"a":1,"b":"bla"})) - json.a.should eq 1 - json.b.should eq "bla" - - json = JsonWithDefaults.from_json(%({"a":1})) - json.a.should eq 1 - json.b.should eq "Haha" - - json = JsonWithDefaults.from_json(%({"b":"bla"})) - json.a.should eq 11 - json.b.should eq "bla" - - json = JsonWithDefaults.from_json(%({})) - json.a.should eq 11 - json.b.should eq "Haha" - - json = JsonWithDefaults.from_json(%({"a":null,"b":null})) - json.a.should eq 11 - json.b.should eq "Haha" - end - - it "bool" do - json = JsonWithDefaults.from_json(%({})) - json.c.should eq true - typeof(json.c).should eq Bool - json.d.should eq false - typeof(json.d).should eq Bool - - json = JsonWithDefaults.from_json(%({"c":false})) - json.c.should eq false - json = JsonWithDefaults.from_json(%({"c":true})) - json.c.should eq true - - json = JsonWithDefaults.from_json(%({"d":false})) - json.d.should eq false - json = JsonWithDefaults.from_json(%({"d":true})) - json.d.should eq true - end - - it "with nilable" do - json = JsonWithDefaults.from_json(%({})) - - json.e.should eq false - typeof(json.e).should eq(Bool | Nil) - - json.f.should eq 1 - typeof(json.f).should eq(Int32 | Nil) - - json.g.should eq nil - typeof(json.g).should eq(Int32 | Nil) - - json = JsonWithDefaults.from_json(%({"e":false})) - json.e.should eq false - json = JsonWithDefaults.from_json(%({"e":true})) - json.e.should eq true - end - - it "create new array every time" do - json = JsonWithDefaults.from_json(%({})) - json.h.should eq [1, 2, 3] - json.h << 4 - json.h.should eq [1, 2, 3, 4] - - json = JsonWithDefaults.from_json(%({})) - json.h.should eq [1, 2, 3] - end - end - - it "uses Time::EpochConverter" do - string = %({"value":1459859781}) - json = JSONWithTimeEpoch.from_json(string) - json.value.should be_a(Time) - json.value.should eq(Time.unix(1459859781)) - json.to_json.should eq(string) - end - - it "uses Time::EpochMillisConverter" do - string = %({"value":1459860483856}) - json = JSONWithTimeEpochMillis.from_json(string) - json.value.should be_a(Time) - json.value.should eq(Time.unix_ms(1459860483856)) - json.to_json.should eq(string) - end - - it "uses JSON::ArrayConverter" do - string = %({"values":[1459859781,1567628762]}) - json = JSONWithArrayConverter.from_json(string) - json.values.should be_a(Array(Time)) - json.values.should eq([Time.unix(1459859781), Time.unix(1567628762)]) - json.to_json.should eq(string) - end - - it "uses JSON::HashValueConverter" do - string = %({"birthdays":{"foo":1459859781,"bar":1567628762}}) - json = JSONWithJSONHashValueConverter.from_json(string) - json.birthdays.should be_a(Hash(String, Time)) - json.birthdays.should eq({"foo" => Time.unix(1459859781), "bar" => Time.unix(1567628762)}) - json.to_json.should eq(string) - end - - it "parses raw value from int" do - string = %({"value":123456789123456789123456789123456789}) - json = JSONWithRaw.from_json(string) - json.value.should eq("123456789123456789123456789123456789") - json.to_json.should eq(string) - end - - it "parses raw value from float" do - string = %({"value":123456789123456789.123456789123456789}) - json = JSONWithRaw.from_json(string) - json.value.should eq("123456789123456789.123456789123456789") - json.to_json.should eq(string) - end - - it "parses raw value from object" do - string = %({"value":[null,true,false,{"x":[1,1.5]}]}) - json = JSONWithRaw.from_json(string) - json.value.should eq(%([null,true,false,{"x":[1,1.5]}])) - json.to_json.should eq(string) - end - - it "parses with root" do - json = %({"result":{"heroes":[{"name":"Batman"}]}}) - result = JSONWithRoot.from_json(json) - result.result.should be_a(Array(JSONPerson)) - result.result.first.name.should eq "Batman" - result.to_json.should eq(json) - end - - it "parses with nilable root" do - json = %({"result":null}) - result = JSONWithNilableRoot.from_json(json) - result.result.should be_nil - result.to_json.should eq("{}") - end - - it "parses with nilable root and emit null" do - json = %({"result":null}) - result = JSONWithNilableRootEmitNull.from_json(json) - result.result.should be_nil - result.to_json.should eq(json) - end - - it "parses nilable union" do - obj = JSONWithNilableUnion.from_json(%({"value": 1})) - obj.value.should eq(1) - obj.to_json.should eq(%({"value":1})) - - obj = JSONWithNilableUnion.from_json(%({"value": null})) - obj.value.should be_nil - obj.to_json.should eq(%({})) - - obj = JSONWithNilableUnion.from_json(%({})) - obj.value.should be_nil - obj.to_json.should eq(%({})) - end - - it "parses nilable union2" do - obj = JSONWithNilableUnion2.from_json(%({"value": 1})) - obj.value.should eq(1) - obj.to_json.should eq(%({"value":1})) - - obj = JSONWithNilableUnion2.from_json(%({"value": null})) - obj.value.should be_nil - obj.to_json.should eq(%({})) - - obj = JSONWithNilableUnion2.from_json(%({})) - obj.value.should be_nil - obj.to_json.should eq(%({})) - end - - describe "parses JSON with presence markers" do - it "parses person with absent attributes" do - json = JSONWithPresence.from_json(%({"first_name": null})) - json.first_name.should be_nil - json.first_name_present?.should be_true - json.last_name.should be_nil - json.last_name_present?.should be_false - end - end - - describe "with query attributes" do - it "defines query getter" do - json = JSONWithQueryAttributes.from_json(%({"foo": true})) - json.foo?.should be_true - json.bar?.should be_false - end - - it "defines query getter with class restriction" do - {% begin %} - {% methods = JSONWithQueryAttributes.methods %} - {{ methods.find(&.name.==("foo?")).return_type }}.should eq(Bool) - {{ methods.find(&.name.==("bar?")).return_type }}.should eq(Bool) - {% end %} - end - - it "defines non-query setter and presence methods" do - json = JSONWithQueryAttributes.from_json(%({"foo": false})) - json.bar_present?.should be_false - json.bar = true - json.bar?.should be_true - end - - it "maps non-query attributes" do - json = JSONWithQueryAttributes.from_json(%({"foo": false, "is_bar": false})) - json.bar_present?.should be_true - json.bar?.should be_false - json.bar = true - json.to_json.should eq(%({"foo":false,"is_bar":true})) - end - - it "raises if non-nilable attribute is nil" do - error_message = <<-'MSG' - Missing JSON attribute: foo - parsing JSONWithQueryAttributes at 1:1 - MSG - ex = expect_raises JSON::MappingError, error_message do - JSONWithQueryAttributes.from_json(%({"is_bar": true})) - end - ex.location.should eq({1, 1}) - end - - it "overwrites non-query attributes" do - json = JSONWithOverwritingQueryAttributes.from_json(%({"foo": true})) - typeof(json.@foo).should eq(Bool) - typeof(json.@bar).should eq(Bool) - end - end - - pending_win32 describe: "BigDecimal" do - it "parses json string with BigDecimal" do - json = JSONWithBigDecimal.from_json(%({"value": "10.05"})) - json.value.should eq(BigDecimal.new("10.05")) - end - - it "parses large json ints with BigDecimal" do - json = JSONWithBigDecimal.from_json(%({"value": 9223372036854775808})) - json.value.should eq(BigDecimal.new("9223372036854775808")) - end - - it "parses json float with BigDecimal" do - json = JSONWithBigDecimal.from_json(%({"value": 10.05})) - json.value.should eq(BigDecimal.new("10.05")) - end - - it "parses large precision json floats with BigDecimal" do - json = JSONWithBigDecimal.from_json(%({"value": 0.00045808999999999997})) - json.value.should eq(BigDecimal.new("0.00045808999999999997")) - end - end -end diff --git a/spec/std/json/serializable_spec.cr b/spec/std/json/serializable_spec.cr index 65ad32f1cbff..3ba818df4c85 100644 --- a/spec/std/json/serializable_spec.cr +++ b/spec/std/json/serializable_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" require "json" +require "yaml" {% unless flag?(:win32) %} - require "yaml" require "big" require "big/json" {% end %} @@ -301,38 +301,36 @@ class JSONAttrModuleTest2 < JSONAttrModuleTest end end -{% unless flag?(:win32) %} - struct JSONAttrPersonWithYAML - include JSON::Serializable - include YAML::Serializable +struct JSONAttrPersonWithYAML + include JSON::Serializable + include YAML::Serializable - property name : String - property age : Int32? + property name : String + property age : Int32? - def initialize(@name : String, @age : Int32? = nil) - end + def initialize(@name : String, @age : Int32? = nil) end +end - struct JSONAttrPersonWithYAMLInitializeHook - include JSON::Serializable - include YAML::Serializable +struct JSONAttrPersonWithYAMLInitializeHook + include JSON::Serializable + include YAML::Serializable - property name : String - property age : Int32? + property name : String + property age : Int32? - def initialize(@name : String, @age : Int32? = nil) - after_initialize - end + def initialize(@name : String, @age : Int32? = nil) + after_initialize + end - @[JSON::Field(ignore: true)] - @[YAML::Field(ignore: true)] - property msg : String? + @[JSON::Field(ignore: true)] + @[YAML::Field(ignore: true)] + property msg : String? - def after_initialize - @msg = "Hello " + name - end + def after_initialize + @msg = "Hello " + name end -{% end %} +end abstract class JSONShape include JSON::Serializable @@ -413,7 +411,7 @@ describe "JSON mapping" do Unknown JSON attribute: foo parsing StrictJSONAttrPerson MSG - ex = expect_raises JSON::MappingError, error_message do + ex = expect_raises ::JSON::SerializableError, error_message do StrictJSONAttrPerson.from_json <<-JSON { "name": "John", @@ -445,7 +443,7 @@ describe "JSON mapping" do Missing JSON attribute: name parsing JSONAttrPerson at 1:1 MSG - ex = expect_raises JSON::MappingError, error_message do + ex = expect_raises ::JSON::SerializableError, error_message do JSONAttrPerson.from_json(%({"age": 30})) end ex.location.should eq({1, 1}) @@ -456,7 +454,7 @@ describe "JSON mapping" do Expected BeginObject but was String at 1:1 parsing StrictJSONAttrPerson at 0:0 MSG - ex = expect_raises JSON::MappingError, error_message do + ex = expect_raises ::JSON::SerializableError, error_message do StrictJSONAttrPerson.from_json <<-JSON "foo" JSON @@ -468,7 +466,7 @@ describe "JSON mapping" do error_message = <<-MSG Couldn't parse (Int32 | Nil) from "foo" at 3:10 MSG - ex = expect_raises JSON::MappingError, error_message do + ex = expect_raises ::JSON::SerializableError, error_message do StrictJSONAttrPerson.from_json <<-JSON { "name": "John", @@ -780,7 +778,7 @@ describe "JSON mapping" do Missing JSON attribute: foo parsing JSONAttrWithQueryAttributes at 1:1 MSG - ex = expect_raises JSON::MappingError, error_message do + ex = expect_raises ::JSON::SerializableError, error_message do JSONAttrWithQueryAttributes.from_json(%({"is_bar": true})) end ex.location.should eq({1, 1}) @@ -816,7 +814,7 @@ describe "JSON mapping" do it { JSONAttrModuleTest2.from_json(%({"bar": 30, "moo": 40})).to_tuple.should eq({40, 15, 30}) } end - pending_win32 "works together with yaml" do + it "works together with yaml" do person = JSONAttrPersonWithYAML.new("Vasya", 30) person.to_json.should eq "{\"name\":\"Vasya\",\"age\":30}" person.to_yaml.should eq "---\nname: Vasya\nage: 30\n" @@ -825,7 +823,7 @@ describe "JSON mapping" do JSONAttrPersonWithYAML.from_yaml(person.to_yaml).should eq person end - pending_win32 "yaml and json with after_initialize hook" do + it "yaml and json with after_initialize hook" do person = JSONAttrPersonWithYAMLInitializeHook.new("Vasya", 30) person.msg.should eq "Hello Vasya" @@ -849,13 +847,13 @@ describe "JSON mapping" do end it "raises if missing discriminator" do - expect_raises(JSON::MappingError, "Missing JSON discriminator field 'type'") do + expect_raises(::JSON::SerializableError, "Missing JSON discriminator field 'type'") do JSONShape.from_json("{}") end end it "raises if unknown discriminator value" do - expect_raises(JSON::MappingError, %(Unknown 'type' discriminator value: "unknown")) do + expect_raises(::JSON::SerializableError, %(Unknown 'type' discriminator value: "unknown")) do JSONShape.from_json(%({"type": "unknown"})) end end diff --git a/spec/std/kernel_spec.cr b/spec/std/kernel_spec.cr index db4a57fab485..998efb9d1d0e 100644 --- a/spec/std/kernel_spec.cr +++ b/spec/std/kernel_spec.cr @@ -1,14 +1,14 @@ require "spec" -require "../spec_helper" +require "./spec_helper" describe "exit" do it "exits normally with status 0" do - status, _ = build_and_run "exit" + status, _ = compile_and_run_source "exit" status.success?.should be_true end it "exits with given error code" do - status, _ = build_and_run "exit 42" + status, _ = compile_and_run_source "exit 42" status.success?.should be_false status.exit_code.should eq(42) end @@ -16,7 +16,7 @@ end describe "at_exit" do it "runs handlers on normal program ending" do - status, output = build_and_run <<-CODE + status, output = compile_and_run_source <<-CODE at_exit do puts "handler code" end @@ -27,7 +27,7 @@ describe "at_exit" do end it "runs handlers on explicit program ending" do - status, output = build_and_run <<-'CODE' + status, output = compile_and_run_source <<-'CODE' at_exit do |exit_code| puts "handler code, exit code: #{exit_code}" end @@ -40,7 +40,7 @@ describe "at_exit" do end it "runs handlers in reverse order" do - status, output = build_and_run <<-CODE + status, output = compile_and_run_source <<-CODE at_exit do puts "first handler code" end @@ -59,7 +59,7 @@ describe "at_exit" do end it "runs all handlers maximum once" do - status, output = build_and_run <<-CODE + status, output = compile_and_run_source <<-CODE at_exit do puts "first handler code" end @@ -86,7 +86,7 @@ describe "at_exit" do end it "allows handlers to change the exit code with explicit `exit` call" do - status, output = build_and_run <<-'CODE' + status, output = compile_and_run_source <<-'CODE' at_exit do |exit_code| puts "first handler code, exit code: #{exit_code}" end @@ -114,7 +114,7 @@ describe "at_exit" do end it "allows handlers to change the exit code with explicit `exit` call (2)" do - status, output = build_and_run <<-'CODE' + status, output = compile_and_run_source <<-'CODE' at_exit do |exit_code| puts "first handler code, exit code: #{exit_code}" end @@ -144,7 +144,7 @@ describe "at_exit" do end it "changes final exit code when an handler raises an error" do - status, output, error = build_and_run <<-'CODE' + status, output, error = compile_and_run_source <<-'CODE' at_exit do |exit_code| puts "first handler code, exit code: #{exit_code}" end @@ -172,19 +172,8 @@ describe "at_exit" do error.should eq "Error running at_exit handler: Raised from at_exit handler!\n" end - it "errors when used in an at_exit handler" do - status, output, error = build_and_run <<-CODE - at_exit do - at_exit {} - end - CODE - - status.success?.should be_false - error.should eq "Error running at_exit handler: Cannot use at_exit from an at_exit handler\n" - end - it "shows unhandled exceptions after at_exit handlers" do - status, _, error = build_and_run <<-CODE + status, _, error = compile_and_run_source <<-CODE at_exit do STDERR.puts "first handler code" end @@ -205,7 +194,7 @@ describe "at_exit" do end it "can get unhandled exception in at_exit handler" do - status, _, error = build_and_run <<-CODE + status, _, error = compile_and_run_source <<-CODE at_exit do |_, ex| STDERR.puts ex.try &.message end @@ -219,11 +208,32 @@ describe "at_exit" do Unhandled exception: Kaboom! OUTPUT end + + it "allows at_exit inside at_exit" do + status, output = compile_and_run_source <<-CODE + at_exit do + puts "1" + at_exit do + puts "2" + end + end + + at_exit do + puts "3" + at_exit do + puts "4" + end + end + CODE + + status.success?.should be_true + output.should eq("3\n4\n1\n2\n") + end end describe "seg fault" do it "reports SIGSEGV" do - status, _, error = build_and_run <<-'CODE' + status, _, error = compile_and_run_source <<-'CODE' puts Pointer(Int64).null.value CODE @@ -241,7 +251,7 @@ describe "seg fault" do # the default stack size is 0.5G. Setting a # smaller stack size with `ulimit -s 8192` # will address this. - status, _, error = build_and_run <<-'CODE' + status, _, error = compile_and_run_source <<-'CODE' def foo y = StaticArray(Int8,512).new(0) foo @@ -255,7 +265,7 @@ describe "seg fault" do {% end %} it "detects stack overflow on a fiber stack" do - status, _, error = build_and_run <<-'CODE' + status, _, error = compile_and_run_source <<-'CODE' def foo y = StaticArray(Int8,512).new(0) foo diff --git a/spec/std/llvm/aarch64_spec.cr b/spec/std/llvm/aarch64_spec.cr index ee30468f0d26..afb682c0c43f 100644 --- a/spec/std/llvm/aarch64_spec.cr +++ b/spec/std/llvm/aarch64_spec.cr @@ -9,6 +9,7 @@ private def abi triple = "aarch64-unknown-linux-gnu" target = LLVM::Target.from_triple(triple) machine = target.create_target_machine(triple) + machine.enable_global_isel = false LLVM::ABI::AArch64.new(machine) end @@ -140,7 +141,7 @@ class LLVM::ABI info = abi.abi_info(arg_types, return_type, true, ctx) info.arg_types.size.should eq(1) - info.arg_types[0].should eq(ArgType.indirect(str, Attribute::ByVal)) + info.arg_types[0].should eq(ArgType.indirect(str, nil)) info.return_type.should eq(ArgType.indirect(str, Attribute::StructRet)) end end diff --git a/spec/std/llvm/arm_abi_spec.cr b/spec/std/llvm/arm_abi_spec.cr index 1bc7d32d5599..98ae9b588a41 100644 --- a/spec/std/llvm/arm_abi_spec.cr +++ b/spec/std/llvm/arm_abi_spec.cr @@ -9,6 +9,7 @@ private def abi triple = "arm-unknown-linux-gnueabihf" target = LLVM::Target.from_triple(triple) machine = target.create_target_machine(triple) + machine.enable_global_isel = false LLVM::ABI::ARM.new(machine) end diff --git a/spec/std/llvm/x86_64_abi_spec.cr b/spec/std/llvm/x86_64_abi_spec.cr index e9dcc70423ce..2e2514e209d2 100644 --- a/spec/std/llvm/x86_64_abi_spec.cr +++ b/spec/std/llvm/x86_64_abi_spec.cr @@ -9,6 +9,7 @@ private def abi triple = LLVM.default_target_triple.gsub(/^(.+?)-/, "x86_64-") target = LLVM::Target.from_triple(triple) machine = target.create_target_machine(triple) + machine.enable_global_isel = false LLVM::ABI::X86_64.new(machine) end diff --git a/spec/std/llvm/x86_abi_spec.cr b/spec/std/llvm/x86_abi_spec.cr index e3ae341756b3..5f69be12df87 100644 --- a/spec/std/llvm/x86_abi_spec.cr +++ b/spec/std/llvm/x86_abi_spec.cr @@ -13,6 +13,7 @@ private def abi {% end %} target = LLVM::Target.from_triple(triple) machine = target.create_target_machine(triple) + machine.enable_global_isel = false LLVM::ABI::X86.new(machine) end diff --git a/spec/std/log/broadcast_backend_spec.cr b/spec/std/log/broadcast_backend_spec.cr index eff16aaf1b62..28b88291855e 100644 --- a/spec/std/log/broadcast_backend_spec.cr +++ b/spec/std/log/broadcast_backend_spec.cr @@ -14,9 +14,9 @@ describe Log::BroadcastBackend do main.append(backend_a, s(:info)) main.append(backend_b, s(:error)) - debug_entry = Log::Entry.new("", s(:debug), "", nil) - info_entry = Log::Entry.new("", s(:info), "", nil) - error_entry = Log::Entry.new("", s(:error), "", nil) + debug_entry = Log::Entry.new("", s(:debug), "", Log::Metadata.empty, nil) + info_entry = Log::Entry.new("", s(:info), "", Log::Metadata.empty, nil) + error_entry = Log::Entry.new("", s(:error), "", Log::Metadata.empty, nil) main.write debug_entry main.write info_entry @@ -68,15 +68,15 @@ describe Log::BroadcastBackend do it "single backend" do main = Log::BroadcastBackend.new - main.append(Log::MemoryBackend.new, s(:warning)) + main.append(Log::MemoryBackend.new, s(:warn)) - main.min_level.should eq(s(:warning)) + main.min_level.should eq(s(:warn)) end it "multiple backends" do main = Log::BroadcastBackend.new main.append(Log::MemoryBackend.new, s(:info)) - main.append(Log::MemoryBackend.new, s(:warning)) + main.append(Log::MemoryBackend.new, s(:warn)) main.min_level.should eq(s(:info)) end diff --git a/spec/std/log/builder_spec.cr b/spec/std/log/builder_spec.cr index 8f97f3b3efb1..d91453044b60 100644 --- a/spec/std/log/builder_spec.cr +++ b/spec/std/log/builder_spec.cr @@ -105,13 +105,13 @@ describe Log::Builder do builder = Log::Builder.new a = Log::MemoryBackend.new builder.bind("*", :fatal, a) - builder.bind("db.*", :warning, a) + builder.bind("db.*", :warn, a) builder.bind("db", :error, a) builder.bind("db.pool", :none, a) builder.for("").level.should eq(s(:fatal)) builder.for("db").level.should eq(s(:error)) - builder.for("db.query").level.should eq(s(:warning)) + builder.for("db.query").level.should eq(s(:warn)) builder.for("db.pool").level.should eq(s(:none)) end @@ -130,10 +130,10 @@ describe Log::Builder do log.level.should eq(s(:none)) a = Log::MemoryBackend.new - builder.bind("*", :warning, a) + builder.bind("*", :warn, a) log.backend.should be(a) - log.level.should eq(s(:warning)) + log.level.should eq(s(:warn)) end it "removes all logs backends on .clear" do diff --git a/spec/std/log/context_spec.cr b/spec/std/log/context_spec.cr index 17e2921ae63a..1b217e8fdb03 100644 --- a/spec/std/log/context_spec.cr +++ b/spec/std/log/context_spec.cr @@ -1,12 +1,11 @@ require "spec" require "log" -require "log/json" -private def c(value) - Log::Context.new(value) +private def m(value) + Log::Metadata.build(value) end -describe Log::Context do +describe "Log.context" do before_each do Log.context.clear end @@ -15,152 +14,80 @@ describe Log::Context do Log.context.clear end - it "initialize" do - c({a: 1}).should eq(c({"a" => c(1)})) - c({a: 1, b: ["str", true], num: 1i64}).should eq(c({"a" => c(1), "b" => c([c("str"), c(true)]), "num" => c(1i64)})) - c({a: 1f32, b: 1f64}).should eq(c({"a" => c(1f32), "b" => c(1f64)})) - t = Time.local - c({time: t}).should eq(c({"time" => c(t)})) - Log::Context.new.should eq(c(NamedTuple.new)) - end - - it "empty" do - Log::Context.empty.should eq(Log::Context.new) - Log::Context.empty.object_id.should_not eq(Log::Context.new.object_id) - Log::Context.empty.object_id.should eq(Log::Context.empty.object_id) - end - - it "validates hash" do - expect_raises(ArgumentError, "Expected hash context, not Int32") do - Log.context = c(1) - end - end + it "can be set and cleared" do + Log.context.metadata.should eq(Log::Metadata.new) - it "immutability" do - context = c({a: 1}) - other = context.as_h - other["a"] = c(2) - - other.should eq({"a" => c(2)}) - context.should eq(c({a: 1})) - end + Log.context.set a: 1 + Log.context.metadata.should eq(m({a: 1})) - it "merge" do - c({a: 1}).merge(c({b: 2})).should eq(c({a: 1, b: 2})) - c({a: 1, b: 3}).merge(c({b: 2})).should eq(c({a: 1, b: 2})) - c({a: 1, b: 3}).merge(c({b: nil})).should eq(c({a: 1, b: nil})) + Log.context.clear + Log.context.metadata.should eq(Log::Metadata.new) end - it "merge against Log::Context.empty without creating a new instance" do - c1 = c({a: 1, b: 3}) - c1.merge(Log::Context.empty).should be(c1) - Log::Context.empty.merge(c1).should be(c1) + it "is extended by set" do + Log.context.set a: 1 + Log.context.set b: 2 + Log.context.metadata.should eq(m({a: 1, b: 2})) end - it "accessors" do - c(nil).as_nil.should be_nil - - c(1).as_i.should eq(1) - - c("a").as_s.should eq("a") - c(1).as_s?.should be_nil - - c(true).as_bool.should eq(true) - c(false).as_bool.should eq(false) - c(true).as_bool?.should eq(true) - c(false).as_bool?.should eq(false) - c(nil).as_bool?.should be_nil + it "existing keys are overwritten by set" do + Log.context.set a: 1, b: 1 + Log.context.set b: 2, c: 3 + Log.context.metadata.should eq(m({a: 1, b: 2, c: 3})) end - describe "implicit context" do - it "can be set and cleared" do - Log.context.should eq(Log::Context.new) + it "is restored after with_context" do + Log.context.set a: 1 - Log.context.set a: 1 - Log.context.should eq(c({a: 1})) - - Log.context.clear - Log.context.should eq(Log::Context.new) - end - - it "is extended by set" do - Log.context.set a: 1 + Log.with_context do Log.context.set b: 2 - Log.context.should eq(c({a: 1, b: 2})) + Log.context.metadata.should eq(m({a: 1, b: 2})) end - it "existing keys are overwritten by set" do - Log.context.set a: 1, b: 1 - Log.context.set b: 2, c: 3 - Log.context.should eq(c({a: 1, b: 2, c: 3})) - end - - it "is restored after with_context" do - Log.context.set a: 1 - - Log.with_context do - Log.context.set b: 2 - Log.context.should eq(c({a: 1, b: 2})) - end - - Log.context.should eq(c({a: 1})) - end - - it "is restored after with_context of Log instance" do - Log.context.set a: 1 - log = Log.for("temp") + Log.context.metadata.should eq(m({a: 1})) + end - log.with_context do - log.context.set b: 2 - log.context.should eq(c({a: 1, b: 2})) - end + it "is restored after with_context of Log instance" do + Log.context.set a: 1 + log = Log.for("temp") - log.context.should eq(c({a: 1})) + log.with_context do + log.context.set b: 2 + log.context.metadata.should eq(m({a: 1, b: 2})) end - it "is per fiber" do - Log.context.set a: 1 - done = Channel(Nil).new - - f = spawn do - Log.context.should eq(Log::Context.new) - Log.context.set b: 2 - Log.context.should eq(c({b: 2})) + log.context.metadata.should eq(m({a: 1})) + end - done.receive - done.receive - end + it "is per fiber" do + Log.context.set a: 1 + done = Channel(Nil).new - done.send nil - Log.context.should eq(c({a: 1})) - done.send nil - end + f = spawn do + Log.context.metadata.should eq(Log::Metadata.new) + Log.context.set b: 2 + Log.context.metadata.should eq(m({b: 2})) - it "is assignable from a hash with symbol keys" do - Log.context.set a: 1 - extra = {:b => 2} - Log.context.set extra - Log.context.should eq(c({a: 1, b: 2})) + done.receive + done.receive end - it "is assignable from a hash with string keys" do - Log.context.set a: 1 - extra = {"b" => 2} - Log.context.set extra - Log.context.should eq(c({a: 1, b: 2})) - end + done.send nil + Log.context.metadata.should eq(m({a: 1})) + done.send nil + end - it "is assignable from a named tuple" do - Log.context.set a: 1 - extra = {b: 2} - Log.context.set extra - Log.context.should eq(c({a: 1, b: 2})) - end + it "is assignable from a hash with symbol keys" do + Log.context.set a: 1 + extra = {:b => 2} + Log.context.set extra + Log.context.metadata.should eq(m({a: 1, b: 2})) + end - it "can be output as JSON" do - raw_context_data = {name: "James"} - Log.context.set raw_context_data - Log.context.to_json.should eq(raw_context_data.to_json) - end + it "is assignable from a named tuple" do + Log.context.set a: 1 + extra = {b: 2} + Log.context.set extra + Log.context.metadata.should eq(m({a: 1, b: 2})) end end diff --git a/spec/std/log/dispatch_spec.cr b/spec/std/log/dispatch_spec.cr new file mode 100644 index 000000000000..1bf5c2260fd2 --- /dev/null +++ b/spec/std/log/dispatch_spec.cr @@ -0,0 +1,57 @@ +require "spec" +require "log" +require "../../support/retry" + +class Log + describe Dispatcher do + it "create dispatcher from enum" do + Dispatcher.for(:direct).should eq(DirectDispatcher) + Dispatcher.for(:async).should be_a(AsyncDispatcher) + Dispatcher.for(:sync).should be_a(SyncDispatcher) + end + end + + describe DirectDispatcher do + it "dispatches entry" do + backend = Log::MemoryBackend.new + backend.dispatcher = DirectDispatcher + backend.dispatch entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + backend.entries.size.should eq(1) + end + end + + describe SyncDispatcher do + it "dispatches entry" do + backend = Log::MemoryBackend.new + backend.dispatcher = SyncDispatcher.new + backend.dispatch entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + backend.entries.size.should eq(1) + end + end + + describe AsyncDispatcher do + it "dispatches entry" do + backend = Log::MemoryBackend.new + backend.dispatcher = AsyncDispatcher.new + backend.dispatch entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + retry { backend.entries.size.should eq(1) } + end + + it "wait for entries to flush before closing" do + backend = Log::MemoryBackend.new + backend.dispatcher = AsyncDispatcher.new + backend.dispatch entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + backend.close + backend.entries.size.should eq(1) + end + + it "can be closed twice" do + backend = Log::MemoryBackend.new + backend.dispatcher = AsyncDispatcher.new + backend.dispatch entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + backend.close + backend.close + backend.entries.size.should eq(1) + end + end +end diff --git a/spec/std/log/env_config_spec.cr b/spec/std/log/env_config_spec.cr index 8eb505a5645d..7bfdfb0f239a 100644 --- a/spec/std/log/env_config_spec.cr +++ b/spec/std/log/env_config_spec.cr @@ -1,28 +1,12 @@ require "spec" require "log" require "log/spec" +require "../../support/env" private def s(value : Log::Severity) value end -private def with_env(**values) - old_values = {} of String => String? - begin - values.each do |key, value| - key = key.to_s - old_values[key] = ENV[key]? - ENV[key] = value - end - - yield - ensure - old_values.each do |key, old_value| - ENV[key] = old_value - end - end -end - describe "Log.setup_from_env" do after_all do # Setup logging in specs (again) since these specs perform Log.setup @@ -63,9 +47,9 @@ describe "Log.setup_from_env" do it "is used if no LOG_LEVEL is set" do with_env "LOG_LEVEL": nil do builder = Log::Builder.new - Log.setup_from_env(builder: builder, default_level: :warning) + Log.setup_from_env(builder: builder, default_level: :warn) - builder.for("").initial_level.should eq(s(:warning)) + builder.for("").initial_level.should eq(s(:warn)) end end diff --git a/spec/std/log/format_spec.cr b/spec/std/log/format_spec.cr new file mode 100644 index 000000000000..feef1beb3874 --- /dev/null +++ b/spec/std/log/format_spec.cr @@ -0,0 +1,88 @@ +require "spec" +require "log" + +class Log + describe ShortFormat do + it "formats an entry" do + entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + io = IO::Memory.new + ShortFormat.format(entry, io) + io.to_s.should match(/^[\d\-.:TZ]+\s* INFO - source: message$/) + end + + it "hides the source if empty" do + entry = Entry.new("", :info, "message", Log::Metadata.empty, nil) + io = IO::Memory.new + ShortFormat.format(entry, io) + io.to_s.should match(/^[\d\-.:TZ]+\s* INFO - message$/) + end + + it "shows the context data" do + entry = Log.with_context do + Log.context.set a: 1, b: 2 + Entry.new("source", :info, "message", Log::Metadata.empty, nil) + end + io = IO::Memory.new + ShortFormat.format(entry, io) + io.to_s.should match(/^[\d\-.:TZ]+\s* INFO - source: message -- a: 1, b: 2$/) + end + + it "shows context and entry data" do + entry = Log.with_context do + Log.context.set a: 1, b: 2 + Entry.new("source", :info, "message", Log::Metadata.build({c: 3, d: 4}), nil) + end + io = IO::Memory.new + ShortFormat.format(entry, io) + io.to_s.should match(/^[\d\-.:TZ]+\s* INFO - source: message -- c: 3, d: 4 -- a: 1, b: 2$/) + end + + it "appends the exception" do + exception = expect_raises(Exception) { raise "foo" } + entry = Entry.new("source", :error, "message", Log::Metadata.empty, exception) + io = IO::Memory.new + ShortFormat.format(entry, io) + io.rewind + io.gets.should match(/^[\d\-.:TZ]+\s* ERROR - source: message$/) + io.gets_to_end.should eq(exception.inspect_with_backtrace) + end + end + + describe ProcFormatter do + it "formats" do + entry = Entry.new("source", :info, "message", Log::Metadata.empty, nil) + io = IO::Memory.new + formatter = Formatter.new do |entry, io| + io << "[" << entry.message << "]" + end + formatter.format(entry, io) + io.to_s.should eq("[message]") + end + end + + define_formatter TestFormatter, "#{severity} #{source(before: '[', after: "] ")}#{progname} #{message}" \ + "#{context(before: " (", after: ')')}#{exception}" + Log.progname = "test" + + describe TestFormatter do + it "formats" do + exception = expect_raises(Exception) { raise "foo" } + entry = Log.with_context do + Log.context.set a: 1, b: 2 + Entry.new("source", :info, "message", Log::Metadata.empty, nil) + end + io = IO::Memory.new + TestFormatter.format(entry, io) + io.puts + TestFormatter.format(Entry.new("", :info, "message", Log::Metadata.empty, nil), io) + io.puts + TestFormatter.format(Entry.new("source", :error, "Oh, no", Log::Metadata.empty, exception), io) + io.rewind + + io.gets.should eq(" INFO [source] test message (a: 1, b: 2)") + io.gets.should eq(" INFO test message") + io.gets.should eq(" ERROR [source] test Oh, no") + io.gets_to_end.should eq(exception.inspect_with_backtrace) + end + end +end diff --git a/spec/std/log/io_backend_spec.cr b/spec/std/log/io_backend_spec.cr index 902d4cedee5a..80f66c08c202 100644 --- a/spec/std/log/io_backend_spec.cr +++ b/spec/std/log/io_backend_spec.cr @@ -1,20 +1,26 @@ -require "spec" +require "../spec_helper" require "log" private def s(value : Log::Severity) value end -private def io_logger(*, stdout : IO, config = nil, source : String = "", progname : String? = nil) +private def io_logger(*, stdout : IO, config = nil, source : String = "") builder = Log::Builder.new backend = Log::IOBackend.new - backend.progname = progname if progname backend.io = stdout builder.bind("*", s(:info), backend) builder.for(source) end describe Log::IOBackend do + pending_win32 "creates with defaults" do + backend = Log::IOBackend.new + backend.io.should eq(STDOUT) + backend.formatter.should eq(Log::ShortFormat) + backend.dispatcher.should be_a(Log::AsyncDispatcher) + end + it "logs messages" do IO.pipe do |r, w| logger = io_logger(stdout: w) @@ -24,7 +30,7 @@ describe Log::IOBackend do logger.level = s(:debug) logger.debug { "debug:show" } - logger.level = s(:warning) + logger.level = s(:warn) logger.debug { "debug:skip:again" } logger.info { "info:skip" } logger.error { "error:show" } @@ -44,7 +50,7 @@ describe Log::IOBackend do logger.info { "info:show" } end - r.gets.should match(/info:show -- {"foo" => "bar"}/) + r.gets.should match(/info:show -- foo: "bar"/) end end @@ -59,10 +65,10 @@ describe Log::IOBackend do it "formats message" do IO.pipe do |r, w| - logger = io_logger(progname: "the-program", stdout: w, source: "db.pool") + logger = io_logger(stdout: w, source: "db.pool") logger.warn { "message" } - r.gets(chomp: false).should match(/W, \[.+? #\d+\] WARNING -- the-program:db.pool: message\n/) + r.gets(chomp: false).should match(/.+? WARN - db.pool: message\n/) end end @@ -89,12 +95,12 @@ describe Log::IOBackend do it "yields message" do IO.pipe do |r, w| - logger = io_logger(stdout: w, progname: "prog", source: "db") + logger = io_logger(stdout: w, source: "db") logger.error { "message" } logger.fatal { "another message" } - r.gets(chomp: false).should match(/ERROR -- prog:db: message\n/) - r.gets(chomp: false).should match(/FATAL -- prog:db: another message\n/) + r.gets(chomp: false).should match(/ERROR - db: message\n/) + r.gets(chomp: false).should match(/FATAL - db: another message\n/) end end end diff --git a/spec/std/log/log_spec.cr b/spec/std/log/log_spec.cr index feffc050d73c..69259bc6576f 100644 --- a/spec/std/log/log_spec.cr +++ b/spec/std/log/log_spec.cr @@ -5,6 +5,10 @@ private def s(value : Log::Severity) value end +private def m(value) + Log::Metadata.build(value) +end + describe Log do before_each do Log.context.clear @@ -19,8 +23,8 @@ describe Log do s(:trace).should be < s(:debug) s(:debug).should be < s(:info) s(:info).should be < s(:notice) - s(:notice).should be < s(:warning) - s(:warning).should be < s(:error) + s(:notice).should be < s(:warn) + s(:warn).should be < s(:error) s(:error).should be < s(:fatal) s(:fatal).should be < s(:none) end @@ -30,7 +34,7 @@ describe Log do Log::Severity.parse("debug").should eq s(:debug) Log::Severity.parse("info").should eq s(:info) Log::Severity.parse("notice").should eq s(:notice) - Log::Severity.parse("warning").should eq s(:warning) + Log::Severity.parse("warn").should eq s(:warn) Log::Severity.parse("error").should eq s(:error) Log::Severity.parse("fatal").should eq s(:fatal) Log::Severity.parse("none").should eq s(:none) @@ -39,7 +43,7 @@ describe Log do Log::Severity.parse("DEBUG").should eq s(:debug) Log::Severity.parse("INFO").should eq s(:info) Log::Severity.parse("NOTICE").should eq s(:notice) - Log::Severity.parse("WARNING").should eq s(:warning) + Log::Severity.parse("WARN").should eq s(:warn) Log::Severity.parse("ERROR").should eq s(:error) Log::Severity.parse("FATAL").should eq s(:fatal) Log::Severity.parse("NONE").should eq s(:none) @@ -48,7 +52,7 @@ describe Log do it "filter messages to the backend above level only" do backend = Log::MemoryBackend.new - log = Log.new("a", backend, :warning) + log = Log.new("a", backend, :warn) log.trace { "trace message" } log.debug { "debug message" } @@ -59,7 +63,7 @@ describe Log do log.fatal { "fatal message" } backend.entries.map { |e| {e.severity, e.message} }.should eq([ - {s(:warning), "warning message"}, + {s(:warn), "warning message"}, {s(:error), "error message"}, {s(:fatal), "fatal message"}, ]) @@ -67,7 +71,7 @@ describe Log do it "level can be changed" do backend = Log::MemoryBackend.new - log = Log.new("a", backend, :warning) + log = Log.new("a", backend, :warn) log.level = :error @@ -110,10 +114,10 @@ describe Log do log.info { "info message" } - backend.entries.first.context.should eq(Log::Context.new({a: 1})) + backend.entries.first.context.should eq(Log::Metadata.build({a: 1})) end - it "context can be changed within the block and is restored" do + it "context can be changed within the block, yet it's not restored" do Log.context.set a: 1 backend = Log::MemoryBackend.new @@ -121,7 +125,137 @@ describe Log do log.info { Log.context.set(b: 2); "info message" } - backend.entries.first.context.should eq(Log::Context.new({a: 1, b: 2})) - Log.context.should eq(Log::Context.new({a: 1})) + backend.entries.first.context.should eq(Log::Metadata.build({a: 1, b: 2})) + Log.context.metadata.should eq(Log::Metadata.build({a: 1, b: 2})) + end + + describe "emitter dsl" do + it "can be used with message" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :debug) + + log.info &.emit("info message") + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:info)) + entry.message.should eq("info message") + entry.data.should eq(Log::Metadata.empty) + entry.exception.should be_nil + end + + it "can be used with message and exception" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :debug) + ex = Exception.new "the attached exception" + + log.debug exception: ex, &.emit("debug message") + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:debug)) + entry.message.should eq("debug message") + entry.data.should eq(Log::Metadata.empty) + entry.exception.should eq(ex) + end + + it "can be used with message and metada explicitly" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :notice) + + log.notice &.emit("notice message", m({a: 1})) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:notice)) + entry.message.should eq("notice message") + entry.data.should eq(m({a: 1})) + entry.exception.should be_nil + end + + it "can be used with message and data via named arguments" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :fatal) + + log.fatal &.emit("fatal message", a: 1) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:fatal)) + entry.message.should eq("fatal message") + entry.data.should eq(m({a: 1})) + entry.exception.should be_nil + end + + it "can be used with message and data via named tuple" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :fatal) + + log.fatal &.emit("fatal message", {a: 1}) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:fatal)) + entry.message.should eq("fatal message") + entry.data.should eq(m({a: 1})) + entry.exception.should be_nil + end + + it "can be used with exception" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :fatal) + ex = Exception.new "the attached exception" + + log.fatal exception: ex, &.emit("fatal message", a: 1) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:fatal)) + entry.message.should eq("fatal message") + entry.data.should eq(m({a: 1})) + entry.exception.should eq(ex) + end + + it "can be used with data only explicitly" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :notice) + + log.notice &.emit(m({a: 1})) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:notice)) + entry.message.should eq("") + entry.data.should eq(m({a: 1})) + entry.exception.should be_nil + end + + it "can be used with data only via named arguments" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :notice) + + log.notice &.emit(a: 1) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:notice)) + entry.message.should eq("") + entry.data.should eq(m({a: 1})) + entry.exception.should be_nil + end + + it "can be used with data only via named tuple" do + backend = Log::MemoryBackend.new + log = Log.new("a", backend, :notice) + + log.notice &.emit(a: 1) + + entry = backend.entries.first + entry.source.should eq("a") + entry.severity.should eq(s(:notice)) + entry.message.should eq("") + entry.data.should eq(m({a: 1})) + entry.exception.should be_nil + end end end diff --git a/spec/std/log/main_spec.cr b/spec/std/log/main_spec.cr index 4abfc88000cc..9b305d2f55ce 100644 --- a/spec/std/log/main_spec.cr +++ b/spec/std/log/main_spec.cr @@ -32,16 +32,16 @@ describe Log do top = Log.for("qux", :info) top.level.should eq(Log::Severity::Info) - Log.for("qux", :warning) - top.level.should eq(Log::Severity::Warning) + Log.for("qux", :warn) + top.level.should eq(Log::Severity::Warn) end it "can build nested with level override" do foo_bar = Log.for("foo").for("bar", :info) foo_bar.level.should eq(Log::Severity::Info) - Log.for("foo.bar", :warning) - foo_bar.level.should eq(Log::Severity::Warning) + Log.for("foo.bar", :warn) + foo_bar.level.should eq(Log::Severity::Warn) end it "can build for module type" do diff --git a/spec/std/log/metadata_spec.cr b/spec/std/log/metadata_spec.cr new file mode 100644 index 000000000000..5319462c0756 --- /dev/null +++ b/spec/std/log/metadata_spec.cr @@ -0,0 +1,123 @@ +require "spec" +require "log" +require "log/json" + +private def m(value) + Log::Metadata.build(value) +end + +private def v(value) + Log::Metadata::Value.new(value) +end + +describe Log::Metadata do + it "empty" do + Log::Metadata.empty.should eq(Log::Metadata.new) + Log::Metadata.empty.object_id.should_not eq(Log::Metadata.new.object_id) + Log::Metadata.empty.object_id.should eq(Log::Metadata.empty.object_id) + end + + it "empty?" do + Log::Metadata.empty.should be_empty + m({} of Symbol => String).should be_empty + Log::Metadata.new.should be_empty + m({} of Symbol => String).extend({} of Symbol => String).should be_empty + + m({a: 1}).should_not be_empty + m({} of Symbol => String).extend({a: 1}).should_not be_empty + m({a: 1}).extend({} of Symbol => String).should_not be_empty + end + + it "extend" do + m({a: 1}).extend({b: 2}).should eq(m({a: 1, b: 2})) + m({a: 1, b: 3}).extend({b: 2}).should eq(m({a: 1, b: 2})) + m({a: 1, b: 3}).extend({b: nil}).should eq(m({a: 1, b: nil})) + end + + it "extend against empty values without creating a new instance" do + c1 = m({a: 1, b: 3}) + c1.extend(NamedTuple.new).should be(c1) + c1.extend(Hash(Symbol, String).new).should be(c1) + end + + it "==" do + m({} of Symbol => String).should eq(m({} of Symbol => String)) + m({a: 1}).should eq(m({a: 1})) + m({a: 1, b: 2}).should eq(m({b: 2, a: 1})) + + m({a: 1}).should_not eq(m({a: 2})) + m({a: 1}).should_not eq(m({b: 1})) + + m({a: 1}).extend({b: 2}).should eq(m({b: 2}).extend({a: 1})) + m({a: 1, b: 1}).extend({b: 2}).should eq(m({b: 2}).extend({a: 1})) + m({a: 1, b: 2}).extend({b: 1}).should eq(m({a: 1, b: 1})) + end + + it "json" do + m({a: 1}).to_json.should eq(%({"a":1})) + m({a: 1, b: 1}).extend({b: 2}).to_json.should eq(%({"b":2,"a":1})) + end + + it "defrags" do + parent = m({a: 1, b: 2}).extend({a: 2}) + md = parent.extend({a: 3}) + + md.@size.should eq(1) + md.@max_total_size.should eq(4) + md.@overriden_size.should eq(1) + md.@parent.should be(parent) + + md.should eq(m({a: 3, b: 2})) + + md.@size.should eq(2) + md.@max_total_size.should eq(2) + md.@overriden_size.should eq(1) + md.@parent.should be_nil + end + + it "[]" do + md = m({a: 1, b: 2}).extend({a: 3}) + + md[:a].should eq(3) + md[:b].should eq(2) + expect_raises(KeyError) { md[:c] } + end + + it "[]?" do + md = m({a: 1, b: 2}).extend({a: 3}) + + md[:a]?.should eq(3) + md[:b]?.should eq(2) + md[:c]?.should be_nil + end +end + +describe Log::Metadata::Value do + it "initialize" do + v({a: 1}).should eq(v({"a" => v(1)})) + v({a: 1, b: ["str", true], num: 1i64}).should eq(v({"a" => v(1), "b" => v([v("str"), v(true)]), "num" => v(1i64)})) + v({a: 1f32, b: 1f64}).should eq(v({"a" => v(1f32), "b" => v(1f64)})) + t = Time.local + v({time: t}).should eq(v({"time" => v(t)})) + v({} of String => String).should eq(v(NamedTuple.new)) + end + + it "accessors" do + v(nil).as_nil.should be_nil + + v(1).as_i.should eq(1) + + v("a").as_s.should eq("a") + v(1).as_s?.should be_nil + + v(true).as_bool.should eq(true) + v(false).as_bool.should eq(false) + v(true).as_bool?.should eq(true) + v(false).as_bool?.should eq(false) + v(nil).as_bool?.should be_nil + end + + it "json" do + v({a: 1}).to_json.should eq(%({"a":1})) + end +end diff --git a/spec/std/logger_spec.cr b/spec/std/logger_spec.cr deleted file mode 100644 index d0b147032cb9..000000000000 --- a/spec/std/logger_spec.cr +++ /dev/null @@ -1,96 +0,0 @@ -require "spec" -require "logger" - -describe "Logger" do - it "logs messages" do - IO.pipe do |r, w| - logger = Logger.new(w) - logger.debug "debug:skip" - logger.info "info:show" - - logger.level = Logger::DEBUG - logger.debug "debug:show" - - logger.level = Logger::WARN - logger.debug "debug:skip:again" - logger.info "info:skip" - logger.error "error:show" - - r.gets.should match(/info:show/) - r.gets.should match(/debug:show/) - r.gets.should match(/error:show/) - end - end - - it "logs any object" do - IO.pipe do |r, w| - logger = Logger.new(w) - logger.info 12345 - - r.gets.should match(/12345/) - end - end - - it "formats message" do - IO.pipe do |r, w| - logger = Logger.new(w) - logger.progname = "crystal" - logger.warn "message" - - r.gets(chomp: false).should match(/W, \[.+? #\d+\] WARN -- crystal: message\n/) - end - end - - it "uses custom formatter" do - IO.pipe do |r, w| - logger = Logger.new(w) - logger.formatter = Logger::Formatter.new do |severity, datetime, progname, message, io| - io << severity.to_s[0] << ' ' << progname << ": " << message - end - logger.warn "message", "prog" - - r.gets(chomp: false).should eq("W prog: message\n") - end - end - - it "yields message" do - IO.pipe do |r, w| - logger = Logger.new(w) - logger.error { "message" } - logger.unknown { "another message" } - - r.gets(chomp: false).should match(/ERROR -- : message\n/) - r.gets(chomp: false).should match(/ ANY -- : another message\n/) - end - end - - it "yields message with progname" do - IO.pipe do |r, w| - logger = Logger.new(w) - logger.error("crystal") { "message" } - logger.unknown("shard") { "another message" } - - r.gets(chomp: false).should match(/ERROR -- crystal: message\n/) - r.gets(chomp: false).should match(/ ANY -- shard: another message\n/) - end - end - - it "can create a logger with nil (#3065)" do - logger = Logger.new(nil) - logger.error("ouch") - end - - it "doesn't yield to the block with nil" do - a = 0 - logger = Logger.new(nil) - logger.info { a = 1 } - a.should eq(0) - end - - it "closes" do - IO.pipe do |r, w| - Logger.new(w).close - w.closed?.should be_true - end - end -end diff --git a/spec/std/named_tuple_spec.cr b/spec/std/named_tuple_spec.cr index 0b49297077ce..7f575b757940 100644 --- a/spec/std/named_tuple_spec.cr +++ b/spec/std/named_tuple_spec.cr @@ -352,6 +352,11 @@ describe "NamedTuple" do tup.keys.should eq({:a, :b}) end + it "does sorted_keys" do + tup = {foo: 1, bar: 2, baz: 3} + tup.sorted_keys.should eq({:bar, :baz, :foo}) + end + it "does values" do tup = {a: 1, b: 'a'} tup.values.should eq({1, 'a'}) diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr index 4a30c2b8d2a0..e1224574efbe 100644 --- a/spec/std/oauth2/client_spec.cr +++ b/spec/std/oauth2/client_spec.cr @@ -40,79 +40,150 @@ describe OAuth2::Client do end describe "get_access_token_using_*" do - it "#get_access_token_using_authorization_code" do - server = HTTP::Server.new do |context| - body = context.request.body.not_nil!.gets_to_end - response = {access_token: "access_token", body: body} - context.response.print response.to_json + describe "using HTTP Basic authentication to pass credentials" do + it "#get_access_token_using_authorization_code" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + + address = server.bind_unused_port "127.0.0.1" + + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") + token.extra.not_nil!["body"].should eq %("redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") + token.access_token.should eq "access_token" + end end - expected = %("client_id=client_id&client_secret=client_secret&redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") - address = server.bind_unused_port "::1" + it "#get_access_token_using_resource_owner_credentials" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end - run_server(server) do - client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + address = server.bind_unused_port "127.0.0.1" - token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") - token.extra.not_nil!["body"].should eq expected - token.access_token.should eq "access_token" + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") + token.extra.not_nil!["body"].should eq %("grant_type=password&username=user123&password=monkey&scope=read_posts") + token.access_token.should eq "access_token" + end end - end - it "#get_access_token_using_resource_owner_credentials" do - server = HTTP::Server.new do |context| - body = context.request.body.not_nil!.gets_to_end - response = {access_token: "access_token", body: body} - context.response.print response.to_json + it "#get_access_token_using_client_credentials" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + + address = server.bind_unused_port "127.0.0.1" + + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + + token = client.get_access_token_using_client_credentials(scope: "read_posts") + token.extra.not_nil!["body"].should eq %("grant_type=client_credentials&scope=read_posts") + token.access_token.should eq "access_token" + end end - expected = %("client_id=client_id&client_secret=client_secret&grant_type=password&username=user123&password=monkey&scope=read_posts") - address = server.bind_unused_port "::1" + it "#get_access_token_using_refresh_token" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + + address = server.bind_unused_port "127.0.0.1" - run_server(server) do - client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" - token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") - token.extra.not_nil!["body"].should eq expected - token.access_token.should eq "access_token" + token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") + token.extra.not_nil!["body"].should eq %("grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") + token.access_token.should eq "access_token" + end end end - - it "#get_access_token_using_client_credentials" do - server = HTTP::Server.new do |context| - body = context.request.body.not_nil!.gets_to_end - response = {access_token: "access_token", body: body} - context.response.print response.to_json + describe "using Request Body to pass credentials" do + it "#get_access_token_using_authorization_code" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + + address = server.bind_unused_port "127.0.0.1" + + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + + token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") + token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") + token.access_token.should eq "access_token" + end end - expected = %("client_id=client_id&client_secret=client_secret&grant_type=client_credentials&scope=read_posts") - address = server.bind_unused_port "::1" + it "#get_access_token_using_resource_owner_credentials" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end - run_server(server) do - client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + address = server.bind_unused_port "127.0.0.1" - token = client.get_access_token_using_client_credentials(scope: "read_posts") - token.extra.not_nil!["body"].should eq expected - token.access_token.should eq "access_token" + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + + token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") + token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=password&username=user123&password=monkey&scope=read_posts") + token.access_token.should eq "access_token" + end end - end - it "#get_access_token_using_refresh_token" do - server = HTTP::Server.new do |context| - body = context.request.body.not_nil!.gets_to_end - response = {access_token: "access_token", body: body} - context.response.print response.to_json + it "#get_access_token_using_client_credentials" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + + address = server.bind_unused_port "127.0.0.1" + + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + + token = client.get_access_token_using_client_credentials(scope: "read_posts") + token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=client_credentials&scope=read_posts") + token.access_token.should eq "access_token" + end end - expected = %("client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") - address = server.bind_unused_port "::1" + it "#get_access_token_using_refresh_token" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + + address = server.bind_unused_port "127.0.0.1" - run_server(server) do - client = OAuth2::Client.new "[::1]", "client_id", "client_secret", port: address.port, scheme: "http" + run_server(server) do + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody - token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") - token.extra.not_nil!["body"].should eq expected - token.access_token.should eq "access_token" + token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") + token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") + token.access_token.should eq "access_token" + end end end end diff --git a/spec/std/object_spec.cr b/spec/std/object_spec.cr index a84557ff3484..65868789abce 100644 --- a/spec/std/object_spec.cr +++ b/spec/std/object_spec.cr @@ -154,6 +154,20 @@ private class HashedTestObject def_hash :a, :b end +private struct NonReflexive + def ==(other) + false + end +end + +private class DefEquals + def initialize + @x = NonReflexive.new + end + + def_equals @x +end + describe Object do describe "delegate" do it "delegates" do @@ -496,4 +510,13 @@ describe Object do it "applies annotation to lazy property (#9139)" do TestObject.test_annotation_count.should eq(1) end + + describe "def_equals" do + it "compares by reference" do + x = DefEquals.new + y = DefEquals.new + (x == x).should be_true + (x == y).should be_false + end + end end diff --git a/spec/std/openssl/digest_spec.cr b/spec/std/openssl/digest_spec.cr index b29fd59af52f..a93f40d829b0 100644 --- a/spec/std/openssl/digest_spec.cr +++ b/spec/std/openssl/digest_spec.cr @@ -10,10 +10,22 @@ describe OpenSSL::Digest do it "should be able to calculate #{algorithm}" do digest = OpenSSL::Digest.new(algorithm) digest << "fooø" + digest.final.hexstring.should eq(expected) + + digest.reset + digest << "fooø" digest.hexdigest.should eq(expected) end end + it "can't call #final more than once" do + digest = OpenSSL::Digest.new("SHA1") + digest.final + expect_raises(Digest::FinalizedError) do + digest.final + end + end + it "raises a UnsupportedError if digest is unsupported" do expect_raises OpenSSL::Digest::UnsupportedError do OpenSSL::Digest.new("unsupported") @@ -44,6 +56,6 @@ describe OpenSSL::Digest do digest << r r.close - digest.hexdigest.should eq("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae") + digest.final.hexstring.should eq("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae") end end diff --git a/spec/std/openssl/ssl/context_spec.cr b/spec/std/openssl/ssl/context_spec.cr index aa357375f77c..68cf7d1076b4 100644 --- a/spec/std/openssl/ssl/context_spec.cr +++ b/spec/std/openssl/ssl/context_spec.cr @@ -41,6 +41,9 @@ describe OpenSSL::SSL::Context do (context.options & OpenSSL::SSL::Options::SINGLE_ECDH_USE).should eq(OpenSSL::SSL::Options::SINGLE_ECDH_USE) (context.options & OpenSSL::SSL::Options::SINGLE_DH_USE).should eq(OpenSSL::SSL::Options::SINGLE_DH_USE) (context.options & OpenSSL::SSL::Options::CIPHER_SERVER_PREFERENCE).should eq(OpenSSL::SSL::Options::CIPHER_SERVER_PREFERENCE) + {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.1.0") >= 0 %} + (context.options & OpenSSL::SSL::Options::NO_RENEGOTIATION).should eq(OpenSSL::SSL::Options::NO_RENEGOTIATION) + {% end %} context.modes.should eq(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS)) context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::NONE) @@ -100,7 +103,7 @@ describe OpenSSL::SSL::Context do pending "uses intermediate default ciphers" do # Can't be checked because `Context#ciphers` is not implemented. - OpenSSL::SSL::Context::Client.new.ciphers.should eq OpenSSL::SSL::Context::CIPHERS_INTERMEDIATE + OpenSSL::SSL::Context::Client.new.ciphers.should eq OpenSSL::SSL::Context::CIPHERS_OLD OpenSSL::SSL::Context::Server.new.ciphers.should eq OpenSSL::SSL::Context::CIPHERS_INTERMEDIATE end diff --git a/spec/std/openssl/ssl/server_spec.cr b/spec/std/openssl/ssl/server_spec.cr index 0fd82271ad0d..ff5e578a8ed0 100644 --- a/spec/std/openssl/ssl/server_spec.cr +++ b/spec/std/openssl/ssl/server_spec.cr @@ -54,7 +54,7 @@ describe OpenSSL::SSL::Server do describe "#accept?" do it "accepts" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair @@ -79,7 +79,7 @@ describe OpenSSL::SSL::Server do describe "#accept" do it "accepts and do handshake" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair @@ -100,7 +100,7 @@ describe OpenSSL::SSL::Server do end it "doesn't to SSL handshake with start_immediately = false" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair @@ -125,7 +125,7 @@ describe OpenSSL::SSL::Server do end it "detects SNI hostname" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair OpenSSL::SSL::Server.open tcp_server, server_context do |server| diff --git a/spec/std/openssl/ssl/socket_spec.cr b/spec/std/openssl/ssl/socket_spec.cr index 40e10d5c31b9..d56d744a3514 100644 --- a/spec/std/openssl/ssl/socket_spec.cr +++ b/spec/std/openssl/ssl/socket_spec.cr @@ -1,12 +1,13 @@ require "spec" require "socket" require "../../spec_helper" +require "../../socket/spec_helper" require "../../../support/ssl" describe OpenSSL::SSL::Socket do describe OpenSSL::SSL::Socket::Server do it "auto accept client by default" do - TCPServer.open(0) do |tcp_server| + TCPServer.open("127.0.0.1", 0) do |tcp_server| server_context, client_context = ssl_context_pair spawn do @@ -23,7 +24,7 @@ describe OpenSSL::SSL::Socket do end it "doesn't accept client when specified" do - TCPServer.open(0) do |tcp_server| + TCPServer.open("127.0.0.1", 0) do |tcp_server| server_context, client_context = ssl_context_pair spawn do @@ -42,7 +43,7 @@ describe OpenSSL::SSL::Socket do end it "returns the cipher that is currently in use" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair OpenSSL::SSL::Server.open(tcp_server, server_context) do |server| @@ -58,7 +59,7 @@ describe OpenSSL::SSL::Socket do end it "returns the TLS version" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair OpenSSL::SSL::Server.open(tcp_server, server_context) do |server| @@ -74,7 +75,7 @@ describe OpenSSL::SSL::Socket do end it "accepts clients that only write then close the connection" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair # in tls 1.3, if clients don't read anything and close the connection # the server still try and write to it a ticket, resulting in a "pipe failure" @@ -96,7 +97,7 @@ describe OpenSSL::SSL::Socket do end it "closes connection to server that doesn't properly terminate SSL session" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair server_context.disable_session_resume_tickets # avoid Broken pipe @@ -116,7 +117,7 @@ describe OpenSSL::SSL::Socket do end it "interprets graceful EOF of underlying socket as SSL termination" do - tcp_server = TCPServer.new(0) + tcp_server = TCPServer.new("127.0.0.1", 0) server_context, client_context = ssl_context_pair server_context.disable_session_resume_tickets # avoid Broken pipe diff --git a/spec/std/option_parser_spec.cr b/spec/std/option_parser_spec.cr index 05653a597686..527c81d840bb 100644 --- a/spec/std/option_parser_spec.cr +++ b/spec/std/option_parser_spec.cr @@ -230,6 +230,22 @@ describe "OptionParser" do end end + it "raises on invalid option if value is given to none value handler (short flag, #9553) " do + expect_raises OptionParser::InvalidOption, "Invalid option: -foo" do + OptionParser.parse(["-foo"]) do |opts| + opts.on("-f", "some flag") { } + end + end + end + + it "raises on invalid option if value is given to none value handler (long flag, #9553)" do + expect_raises OptionParser::InvalidOption, "Invalid option: --foo=bar" do + OptionParser.parse(["--foo=bar"]) do |opts| + opts.on("-foo", "some flag") { } + end + end + end + it "calls the handler for invalid options" do called = false OptionParser.parse(["-f", "-j"]) do |opts| @@ -572,6 +588,16 @@ describe "OptionParser" do USAGE end + it "handles subcommands with hyphen" do + subcommand = false + OptionParser.parse(%w(sub-command)) do |opts| + opts.banner = "Usage: foo" + opts.on("sub-command", "Subcommand description") { subcommand = true } + end + + subcommand.should be_true + end + it "stops when asked" do args = %w(--foo --stop --bar) foo = false diff --git a/spec/std/overflow_spec.cr b/spec/std/overflow_spec.cr new file mode 100644 index 000000000000..460e4882a23a --- /dev/null +++ b/spec/std/overflow_spec.cr @@ -0,0 +1,54 @@ +{% skip_file unless compare_versions(Crystal::VERSION, "0.35.0-0") > 0 %} + +require "big" +require "spec" + +{% for i in Int::Signed.union_types %} + struct {{i}} + TEST_CASES = [MIN, MIN &+ 1, MIN &+ 2, -1, 0, 1, MAX &- 2, MAX &- 1, MAX] of {{i}} + end +{% end %} + +{% for i in Int::Unsigned.union_types %} + struct {{i}} + TEST_CASES = [MIN, MIN &+ 1, MIN &+ 2, MAX &- 2, MAX &- 1, MAX] of {{i}} + end +{% end %} + +macro run_op_tests(t, u, op) + it "overflow test #{{{t}}} #{{{op}}} #{{{u}}}" do + {{t}}::TEST_CASES.each do |lhs| + {{u}}::TEST_CASES.each do |rhs| + result = lhs.to_big_i {{op.id}} rhs.to_big_i + passes = {{t}}::MIN <= result <= {{t}}::MAX + begin + if passes + (lhs {{op.id}} rhs).should eq(lhs &{{op.id}} rhs) + else + expect_raises(OverflowError) { lhs {{op.id}} rhs } + end + rescue e : Spec::AssertionFailed + raise Spec::AssertionFailed.new("#{e.message}: #{lhs} #{{{op}}} #{rhs}", e.file, e.line) + rescue e : OverflowError + raise OverflowError.new("#{e.message}: #{lhs} #{{{op}}} #{rhs}") + end + end + end + end +end + +{% if flag?(:darwin) %} + private OVERFLOW_TEST_TYPES = [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128] +{% else %} + private OVERFLOW_TEST_TYPES = [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64] +{% end %} + +describe "overflow" do + {% for t in OVERFLOW_TEST_TYPES %} + {% for u in OVERFLOW_TEST_TYPES %} + run_op_tests {{t}}, {{u}}, :+ + run_op_tests {{t}}, {{u}}, :- + run_op_tests {{t}}, {{u}}, :* + {% end %} + {% end %} +end diff --git a/spec/std/process/find_executable_spec.cr b/spec/std/process/find_executable_spec.cr new file mode 100644 index 000000000000..cb5033f13ccd --- /dev/null +++ b/spec/std/process/find_executable_spec.cr @@ -0,0 +1,141 @@ +require "spec" +require "../../support/tempfile" + +{% if flag?(:win32) %} + FIND_EXECUTABLE_TEST_FILES = { + [ + "inbase.exe", + "not_exe", + ".exe", + "inboth.exe", + + "inbasebat.bat", + "inbase.foo.exe", + ".inbase.exe", + + "sub/insub.exe", + "sub/not_exe", + "sub/.exe", + + "../path/inpath.exe", + "../path/not_exe", + "../path/.exe", + "../path/inboth.exe", + ], [] of String, + } + + def find_executable_test_cases(pwd) + pwd_nodrive = "\\#{pwd.relative_to(pwd.anchor.not_nil!)}" + { + "inbase.exe" => "inbase.exe", + "inbase" => "inbase.exe", + "sub\\insub.exe" => "sub/insub.exe", + "sub/insub" => "sub/insub.exe", + "inpath.exe" => "../path/inpath.exe", + "inpath" => "../path/inpath.exe", + "sub/.exe" => "sub/.exe", + "sub\\" => "sub/.exe", + "sub/" => "sub/.exe", + ".exe" => ".exe", + "not_exe" => nil, + "sub\\not_exe" => nil, + "inbasebat" => nil, + "inbase.foo.exe" => "inbase.foo.exe", + "inbase.foo" => nil, + ".inbase.exe" => ".inbase.exe", + ".inbase" => nil, + "" => nil, + "." => nil, + "inboth.exe" => "inboth.exe", + "inboth" => "inboth.exe", + "./inbase" => "inbase.exe", + "../base/inbase" => "inbase.exe", + "./inpath" => nil, + "sub" => nil, + "#{pwd}\\sub" => nil, + "#{pwd}\\sub\\" => nil, + # 'C:\Temp\base\inbase', 'C:\Temp\base\.exe', 'C:\Temp\base\' + "#{pwd}\\inbase" => "inbase.exe", + "#{pwd}\\.exe" => ".exe", + "#{pwd}\\" => nil, + # 'C:inbase', 'C:.exe', 'C:' + "#{pwd.drive}inbase" => "inbase.exe", + "#{pwd.drive}.exe" => ".exe", + "#{pwd.drive}" => nil, + "#{pwd.drive}sub\\" => nil, + # '\Temp\base\inbase', '\Temp\base\.exe', '\Temp\base\' + "#{pwd_nodrive}\\inbase" => "inbase.exe", + "#{pwd_nodrive}\\.exe" => ".exe", + "#{pwd_nodrive}\\" => nil, + } + end +{% else %} + FIND_EXECUTABLE_TEST_FILES = { + [ + "inbase", + "sub/insub", + "../path/inpath", + ], [ + "not_exe", + "sub/not_exe", + "../path/not_exe", + ], + } + + def find_executable_test_cases(pwd) + { + "./inbase" => "inbase", + "../base/inbase" => "inbase", + "inbase" => nil, + "sub/insub" => "sub/insub", + "inpath" => "../path/inpath", + "./inpath" => nil, + "inbase/" => nil, + "sub/insub/" => nil, + "./not_exe" => nil, + "not_exe" => nil, + "sub/not_exe" => nil, + "" => nil, + "." => nil, + "#{pwd}/inbase" => "inbase", + "#{pwd}/inbase/" => nil, + "#{pwd}/sub" => nil, + "./sub" => nil, + "sub" => nil, + } + end +{% end %} + +describe "Process.find_executable" do + test_dir = Path[SPEC_TEMPFILE_PATH] / "find_executable" + base_dir = Path[test_dir] / "base" + path_dir = Path[test_dir] / "path" + + around_all do |all| + exe_names, non_exe_names = FIND_EXECUTABLE_TEST_FILES + (exe_names + non_exe_names).each do |name| + Dir.mkdir_p((base_dir / name).parent) + File.write(base_dir / name, "") + end + exe_names.each do |name| + File.chmod(base_dir / name, 0o755) + end + + all.run + + FileUtils.rm_r(test_dir.to_s) + end + + find_executable_test_cases(base_dir).each do |(command, exp)| + if exp + exp_path = File.expand_path(exp, base_dir) + it "finds '#{command}' as '#{exp}'" do + Process.find_executable(command, path: path_dir.to_s, pwd: base_dir).should eq exp_path + end + else + it "fails to find '#{command}'" do + Process.find_executable(command, path: path_dir.to_s, pwd: base_dir).should be_nil + end + end + end +end diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 135f6d259d4f..77ac0da19944 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -1,7 +1,6 @@ require "spec" require "process" require "./spec_helper" -require "../spec_helper" private def exit_code_command(code) {% if flag?(:win32) %} @@ -34,6 +33,15 @@ private def stdin_to_stdout_command {% end %} end +private def print_env_command + {% if flag?(:win32) %} + # cmd adds these by itself, clear them out before printing. + shell_command("set COMSPEC=& set PATHEXT=& set PROMPT=& set") + {% else %} + {"env", [] of String} + {% end %} +end + private def standing_command {% if flag?(:win32) %} {"cmd.exe"} @@ -61,9 +69,28 @@ describe Process do process.wait.exit_code.should eq(1) end + it "raises if command doesn't exist" do + expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do + Process.new("foobarbaz") + end + end + + pending_win32 "raises if command is not executable" do + with_tempfile("crystal-spec-run") do |path| + File.touch path + expect_raises(File::AccessDeniedError, "Error executing process: '#{path.inspect_unquoted}'") do + Process.new(path) + end + end + end + it "raises if command could not be executed" do - expect_raises(RuntimeError, "Error executing process:") do - Process.new("foobarbaz", ["foo"]) + with_tempfile("crystal-spec-run") do |path| + File.touch path + command = File.join(path, "foo") + expect_raises(IO::Error, "Error executing process: '#{command.inspect_unquoted}'") do + Process.new(command) + end end end @@ -138,7 +165,7 @@ describe Process do end pending_win32 "chroot raises when unprivileged" do - status, output = build_and_run <<-'CODE' + status, output = compile_and_run_source <<-'CODE' begin Process.chroot("/usr") puts "FAIL" @@ -190,28 +217,99 @@ describe Process do end describe "environ" do - pending_win32 "clears the environment" do - value = Process.run("env", clear_env: true) do |proc| + it "clears the environment" do + value = Process.run(*print_env_command, clear_env: true) do |proc| proc.output.gets_to_end end value.should eq("") end - pending_win32 "sets an environment variable" do - env = {"FOO" => "bar"} - value = Process.run("env", clear_env: true, env: env) do |proc| + it "clears and sets an environment variable" do + value = Process.run(*print_env_command, clear_env: true, env: {"FOO" => "bar"}) do |proc| proc.output.gets_to_end end - value.should eq("FOO=bar\n") + value.should eq("FOO=bar#{newline}") end - pending_win32 "deletes an environment variable" do - env = {"HOME" => nil} - value = Process.run("env | egrep '^HOME='", env: env, shell: true) do |proc| + it "sets an environment variable" do + value = Process.run(*print_env_command, env: {"FOO" => "bar"}) do |proc| proc.output.gets_to_end end - value.should eq("") + value.should match /(*ANYCRLF)^FOO=bar$/m + end + + it "sets an empty environment variable" do + value = Process.run(*print_env_command, env: {"FOO" => ""}) do |proc| + proc.output.gets_to_end + end + value.should match /(*ANYCRLF)^FOO=$/m + end + + it "deletes existing environment variable" do + ENV["FOO"] = "bar" + value = Process.run(*print_env_command, env: {"FOO" => nil}) do |proc| + proc.output.gets_to_end + end + value.should_not match /(*ANYCRLF)^FOO=/m + ensure + ENV.delete("FOO") + end + + {% if flag?(:win32) %} + it "deletes existing environment variable case-insensitive" do + ENV["FOO"] = "bar" + value = Process.run(*print_env_command, env: {"foo" => nil}) do |proc| + proc.output.gets_to_end + end + value.should_not match /(*ANYCRLF)^FOO=/mi + ensure + ENV.delete("FOO") + end + {% end %} + + it "preserves existing environment variable" do + ENV["FOO"] = "bar" + value = Process.run(*print_env_command) do |proc| + proc.output.gets_to_end + end + value.should match /(*ANYCRLF)^FOO=bar$/m + ensure + ENV.delete("FOO") end + + it "preserves and sets an environment variable" do + ENV["FOO"] = "bar" + value = Process.run(*print_env_command, env: {"FOO2" => "bar2"}) do |proc| + proc.output.gets_to_end + end + value.should match /(*ANYCRLF)^FOO=bar$/m + value.should match /(*ANYCRLF)^FOO2=bar2$/m + ensure + ENV.delete("FOO") + end + + it "overrides existing environment variable" do + ENV["FOO"] = "bar" + value = Process.run(*print_env_command, env: {"FOO" => "different"}) do |proc| + proc.output.gets_to_end + end + value.should match /(*ANYCRLF)^FOO=different$/m + ensure + ENV.delete("FOO") + end + + {% if flag?(:win32) %} + it "overrides existing environment variable case-insensitive" do + ENV["FOO"] = "bar" + value = Process.run(*print_env_command, env: {"fOo" => "different"}) do |proc| + proc.output.gets_to_end + end + value.should_not match /(*ANYCRLF)^FOO=/m + value.should match /(*ANYCRLF)^fOo=different$/m + ensure + ENV.delete("FOO") + end + {% end %} end describe "signal" do @@ -259,6 +357,12 @@ describe Process do File.exists?(path).should be_true end end + + it "gets error from exec" do + expect_raises(File::NotFoundError, "Error executing process: 'foobarbaz'") do + Process.exec("foobarbaz") + end + end {% end %} pending_win32 "checks for existence" do @@ -298,27 +402,67 @@ describe Process do end end - describe "find_executable" do - pwd = Process::INITIAL_PWD - crystal_path = Path.new(pwd, "bin", "crystal").to_s - - pending_win32 "resolves absolute executable" do - Process.find_executable(Path.new(pwd, "bin", "crystal")).should eq(crystal_path) + describe "quote_posix" do + it { Process.quote_posix("").should eq "''" } + it { Process.quote_posix(" ").should eq "' '" } + it { Process.quote_posix("$hi").should eq "'$hi'" } + it { Process.quote_posix(orig = "aZ5+,-./:=@_").should eq orig } + it { Process.quote_posix(orig = "cafe").should eq orig } + it { Process.quote_posix("café").should eq "'café'" } + it { Process.quote_posix("I'll").should eq %('I'"'"'ll') } + it { Process.quote_posix("'").should eq %(''"'"'') } + it { Process.quote_posix("\\").should eq "'\\'" } + + context "join" do + it { Process.quote_posix([] of String).should eq "" } + it { Process.quote_posix(["my file.txt", "another.txt"]).should eq "'my file.txt' another.txt" } + it { Process.quote_posix(["foo ", "", " ", " bar"]).should eq "'foo ' '' ' ' ' bar'" } + it { Process.quote_posix(["foo'", "\"bar"]).should eq %('foo'"'"'' '"bar') } end + end - pending_win32 "resolves relative executable" do - Process.find_executable(Path.new("bin", "crystal")).should eq(crystal_path) - Process.find_executable(Path.new("..", File.basename(pwd), "bin", "crystal")).should eq(crystal_path) + describe "quote_windows" do + it { Process.quote_windows("").should eq %("") } + it { Process.quote_windows(" ").should eq %(" ") } + it { Process.quote_windows(orig = "%hi%").should eq orig } + it { Process.quote_windows(%q(C:\"foo" project.txt)).should eq %q("C:\\\"foo\" project.txt") } + it { Process.quote_windows(%q(C:\"foo"_project.txt)).should eq %q(C:\\\"foo\"_project.txt) } + it { Process.quote_windows(%q(C:\Program Files\Foo Bar\foobar.exe)).should eq %q("C:\Program Files\Foo Bar\foobar.exe") } + it { Process.quote_windows(orig = "café").should eq orig } + it { Process.quote_windows(%(")).should eq %q(\") } + it { Process.quote_windows(%q(a\\b\ c\)).should eq %q("a\\b\ c\\") } + it { Process.quote_windows(orig = %q(a\\b\c\)).should eq orig } + + context "join" do + it { Process.quote_windows([] of String).should eq "" } + it { Process.quote_windows(["my file.txt", "another.txt"]).should eq %("my file.txt" another.txt) } + it { Process.quote_windows(["foo ", "", " ", " bar"]).should eq %("foo " "" " " " bar") } end + end - pending_win32 "searches within PATH" do - (path = Process.find_executable("ls")).should_not be_nil - path.not_nil!.should match(/#{File::SEPARATOR}ls$/) - - (path = Process.find_executable("crystal")).should_not be_nil - path.not_nil!.should match(/#{File::SEPARATOR}crystal$/) + describe "parse_arguments" do + it { Process.parse_arguments("").should eq(%w[]) } + it { Process.parse_arguments(" ").should eq(%w[]) } + it { Process.parse_arguments("foo").should eq(%w[foo]) } + it { Process.parse_arguments("foo bar").should eq(%w[foo bar]) } + it { Process.parse_arguments(%q("foo bar" 'foo bar' baz)).should eq(["foo bar", "foo bar", "baz"]) } + it { Process.parse_arguments(%q("foo bar"'foo bar'baz)).should eq(["foo barfoo barbaz"]) } + it { Process.parse_arguments(%q(foo\ bar)).should eq(["foo bar"]) } + it { Process.parse_arguments(%q("foo\ bar")).should eq(["foo\\ bar"]) } + it { Process.parse_arguments(%q('foo\ bar')).should eq(["foo\\ bar"]) } + it { Process.parse_arguments("\\").should eq(["\\"]) } + it { Process.parse_arguments(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq(["foo bar", "\\hello/", "Fizz Buzz"]) } + + it "raises an error when double quote is unclosed" do + expect_raises ArgumentError, "Unmatched quote" do + Process.parse_arguments(%q["foo]) + end + end - Process.find_executable("some_very_unlikely_file_to_exist").should be_nil + it "raises an error if single quote is unclosed" do + expect_raises ArgumentError, "Unmatched quote" do + Process.parse_arguments(%q['foo]) + end end end end diff --git a/spec/std/range_spec.cr b/spec/std/range_spec.cr index 75fae97a2665..ffa28a209db4 100644 --- a/spec/std/range_spec.cr +++ b/spec/std/range_spec.cr @@ -28,6 +28,18 @@ struct RangeSpecIntWrapper end end +private def range_endless_each + (2..).each do |x| + return x + end +end + +private def range_beginless_reverse_each + (..2).reverse_each do |x| + return x + end +end + describe "Range" do it "initialized with new method" do Range.new(1, 10).should eq(1..10) @@ -215,6 +227,14 @@ describe "Range" do range.each { } end end + + it "doesn't have Nil as a type for endless each" do + typeof(range_endless_each).should eq(Int32) + end + + it "doesn't have Nil as a type for beginless each" do + typeof(range_beginless_reverse_each).should eq(Int32) + end end describe "reverse_each" do diff --git a/spec/std/socket/address_spec.cr b/spec/std/socket/address_spec.cr index 59167fc9c041..c72e7c0bdf13 100644 --- a/spec/std/socket/address_spec.cr +++ b/spec/std/socket/address_spec.cr @@ -244,4 +244,15 @@ describe Socket do Socket.ip?("::0::ffff:c0a8:5e4").should be_false Socket.ip?("c0a8").should be_false end + + it "==" do + a = Socket::IPAddress.new("127.0.0.1", 8080) + b = Socket::UNIXAddress.new("some_path") + c = "sonme_path" + (a == a).should be_true + (b == b).should be_true + (a == b).should be_false + (a == c).should be_false + (b == c).should be_false + end end diff --git a/spec/std/socket/socket_spec.cr b/spec/std/socket/socket_spec.cr index 9b9ecb146c60..2ab9ea5f6f82 100644 --- a/spec/std/socket/socket_spec.cr +++ b/spec/std/socket/socket_spec.cr @@ -42,12 +42,23 @@ describe Socket do end end + it "accept raises timeout error if read_timeout is specified" do + server = Socket.new(Socket::Family::INET, Socket::Type::STREAM, Socket::Protocol::TCP) + port = unused_local_port + server.bind("0.0.0.0", port) + server.read_timeout = 0.1 + server.listen + + expect_raises(IO::TimeoutError) { server.accept } + expect_raises(IO::TimeoutError) { server.accept? } + end + it "sends messages" do port = unused_local_port - server = Socket.tcp(Socket::Family::INET6) - server.bind("::1", port) + server = Socket.tcp(Socket::Family::INET) + server.bind("127.0.0.1", port) server.listen - address = Socket::IPAddress.new("::1", port) + address = Socket::IPAddress.new("127.0.0.1", port) spawn do client = server.not_nil!.accept client.gets.should eq "foo" @@ -55,7 +66,7 @@ describe Socket do ensure client.try &.close end - socket = Socket.tcp(Socket::Family::INET6) + socket = Socket.tcp(Socket::Family::INET) socket.connect(address) socket.puts "foo" socket.gets.should eq "bar" diff --git a/spec/std/socket/spec_helper.cr b/spec/std/socket/spec_helper.cr index bfd016f4129a..486e4a142ee7 100644 --- a/spec/std/socket/spec_helper.cr +++ b/spec/std/socket/spec_helper.cr @@ -1,9 +1,20 @@ require "spec" require "socket" -def unused_local_port - TCPServer.open("::", 0) do |server| - server.local_address.port +module SocketSpecHelper + class_getter?(supports_ipv6 : Bool) do + TCPServer.open("::1", 0) { return true } + false + rescue Socket::BindError + false + end +end + +def pending_ipv6(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) + if SocketSpecHelper.supports_ipv6? + it(description, file: file, line: line, end_line: end_line, &block) + else + pending(description, file: file, line: line, end_line: end_line) end end @@ -12,7 +23,17 @@ def each_ip_family(&block : Socket::Family, String, String ->) block.call Socket::Family::INET, "127.0.0.1", "0.0.0.0" end - describe "using IPv6" do - block.call Socket::Family::INET6, "::1", "::" + if SocketSpecHelper.supports_ipv6? + describe "using IPv6" do + block.call Socket::Family::INET6, "::1", "::" + end + else + pending "using IPv6" + end +end + +def unused_local_port + TCPServer.open("::", 0) do |server| + server.local_address.port end end diff --git a/spec/std/socket/udp_socket_spec.cr b/spec/std/socket/udp_socket_spec.cr index 087acca25aa5..b99e4d286b81 100644 --- a/spec/std/socket/udp_socket_spec.cr +++ b/spec/std/socket/udp_socket_spec.cr @@ -78,14 +78,32 @@ describe UDPSocket do expect_raises(Socket::Error, "Unsupported IP address family: INET. For use with IPv6 only") do udp.multicast_interface 0 end - udp.multicast_interface Socket::IPAddress.new(unspecified_address, 0) + + begin + udp.multicast_interface Socket::IPAddress.new(unspecified_address, 0) + rescue e : Socket::Error + if e.os_error == Errno::ENOPROTOOPT + pending!("Multicast device selection not available on this host") + else + raise e + end + end Socket::IPAddress.new("224.0.0.254", port) when Socket::Family::INET6 expect_raises(Socket::Error, "Unsupported IP address family: INET6. For use with IPv4 only") do udp.multicast_interface(Socket::IPAddress.new(unspecified_address, 0)) end - udp.multicast_interface(0) + + begin + udp.multicast_interface(0) + rescue e : Socket::Error + if e.os_error == Errno::ENOPROTOOPT + pending!("Multicast device selection not available on this host") + else + raise e + end + end Socket::IPAddress.new("ff02::102", port) else diff --git a/spec/std/spec/hooks_spec.cr b/spec/std/spec/hooks_spec.cr new file mode 100644 index 000000000000..105f5bd9db0e --- /dev/null +++ b/spec/std/spec/hooks_spec.cr @@ -0,0 +1,145 @@ +require "./spec_helper" + +describe Spec do + describe "hooks" do + it "runs in correct order" do + compile_and_run_source(<<-CR, flags: %w(--no-debug))[1].lines[..-5].should eq <<-OUT.lines + require "prelude" + require "spec" + + begin + before_all {} + rescue exc + puts exc.message + end + begin + before_each {} + rescue exc + puts exc.message + end + begin + after_all {} + rescue exc + puts exc.message + end + begin + after_each {} + rescue exc + puts exc.message + end + begin + around_all {} + rescue exc + puts exc.message + end + begin + around_each {} + rescue exc + puts exc.message + end + + Spec.before_suite { puts "Spec:before_suite" } + Spec.before_each { puts "Spec:before_each" } + Spec.after_suite { puts "Spec:after_all" } + Spec.after_each { puts "Spec:after_each" } + Spec.around_each do |example| + puts "Spec:around_each:before" + example.run + puts "Spec:around_each:after" + end + + describe "foo" do + before_all { puts "foo:before_all" } + before_each { puts "foo:before_each" } + after_all { puts "foo:after_all" } + after_each { puts "foo:after_each" } + around_all do |example| + puts "foo:around_all:before" + example.run + puts "foo:around_all:after" + end + around_each do |example| + puts "foo:around_each:before" + example.run + puts "foo:around_each:after" + end + + it {} + it {} + + describe "foofoo" do + it {} + end + end + + describe "bar" do + before_all { puts "bar:before_all" } + before_each { puts "bar:before_each" } + after_all { puts "bar:after_all" } + after_each { puts "bar:after_each" } + around_all do |example| + puts "bar:around_all:before" + example.run + puts "bar:around_all:after" + end + around_each do |example| + puts "bar:around_each:before" + example.run + puts "bar:around_each:after" + end + + it {} + end + CR + Can't call `before_all` outside of a describe/context + Can't call `before_each` outside of a describe/context + Can't call `after_all` outside of a describe/context + Can't call `after_each` outside of a describe/context + Can't call `around_all` outside of a describe/context + Can't call `around_each` outside of a describe/context + Spec:before_suite + foo:around_all:before + foo:before_all + Spec:around_each:before + foo:around_each:before + Spec:before_each + foo:before_each + .foo:after_each + Spec:after_each + foo:around_each:after + Spec:around_each:after + Spec:around_each:before + foo:around_each:before + Spec:before_each + foo:before_each + .foo:after_each + Spec:after_each + foo:around_each:after + Spec:around_each:after + Spec:around_each:before + foo:around_each:before + Spec:before_each + foo:before_each + .foo:after_each + Spec:after_each + foo:around_each:after + Spec:around_each:after + foo:after_all + foo:around_all:after + bar:around_all:before + bar:before_all + Spec:around_each:before + bar:around_each:before + Spec:before_each + bar:before_each + .bar:after_each + Spec:after_each + bar:around_each:after + Spec:around_each:after + bar:after_all + bar:around_all:after + Spec:after_all + OUT + end + end +end diff --git a/spec/std/spec/junit_formatter_spec.cr b/spec/std/spec/junit_formatter_spec.cr index 734255438d12..d2f5cd9fcc75 100644 --- a/spec/std/spec/junit_formatter_spec.cr +++ b/spec/std/spec/junit_formatter_spec.cr @@ -1,7 +1,5 @@ require "../spec_helper" -{% unless flag?(:win32) %} - require "xml" -{% end %} +require "xml" class Spec::JUnitFormatter property started_at @@ -103,7 +101,7 @@ describe "JUnit Formatter" do output.should eq(expected) end - pending_win32 "encodes class names from the relative file path" do + it "encodes class names from the relative file path" do output = build_report do |f| f.report Spec::Result.new(:success, "foo", __FILE__, __LINE__, nil, nil) end @@ -112,7 +110,7 @@ describe "JUnit Formatter" do classname.should eq("spec.std.spec.junit_formatter_spec") end - pending_win32 "outputs timestamp according to RFC 3339" do + it "outputs timestamp according to RFC 3339" do now = Time.utc output = build_report(timestamp: now) do |f| @@ -123,7 +121,7 @@ describe "JUnit Formatter" do classname.should eq(now.to_rfc3339) end - pending_win32 "escapes spec names" do + it "escapes spec names" do output = build_report do |f| f.report Spec::Result.new(:success, %(complicated " '&ame), __FILE__, __LINE__, nil, nil) f.report Spec::Result.new(:success, %(ctrl characters follow - \r\n), __FILE__, __LINE__, nil, nil) @@ -136,7 +134,7 @@ describe "JUnit Formatter" do name.should eq(%(ctrl characters follow - \\r\\n)) end - pending_win32 "report failure stacktrace if present" do + it "report failure stacktrace if present" do cause = exception_with_backtrace("Something happened") output = build_report do |f| @@ -151,7 +149,7 @@ describe "JUnit Formatter" do backtrace.should eq(cause.backtrace.join('\n')) end - pending_win32 "report error stacktrace if present" do + it "report error stacktrace if present" do cause = exception_with_backtrace("Something happened") output = build_report do |f| diff --git a/spec/std/spec_helper.cr b/spec/std/spec_helper.cr index b8f2704f08ea..9b67c2b4b9a7 100644 --- a/spec/std/spec_helper.cr +++ b/spec/std/spec_helper.cr @@ -1,29 +1,12 @@ require "spec" require "../support/tempfile" require "../support/fibers" +require "../support/win32" def datapath(*components) File.join("spec", "std", "data", *components) end -{% if flag?(:win32) %} - def pending_win32(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) - pending("#{description} [win32]", file, line, end_line) - end - - def pending_win32(*, describe, file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) - pending_win32(describe, file, line, end_line) { } - end -{% else %} - def pending_win32(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) - it(description, file, line, end_line, &block) - end - - def pending_win32(*, describe, file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) - describe(describe, file, line, end_line, &block) - end -{% end %} - private class Witness @checked = false @@ -91,3 +74,48 @@ def spawn_and_check(before : Proc(_), file = __FILE__, line = __LINE__, &block : fail "Failed to stress expected path", file, line end end + +def compile_file(source_file, flags = %w(), file = __FILE__) + with_temp_executable("executable_file", file: file) do |executable_file| + Process.run("bin/crystal", ["build"] + flags + ["-o", executable_file, source_file]) + File.exists?(executable_file).should be_true + + yield executable_file + end +end + +def compile_source(source, flags = %w(), file = __FILE__) + with_tempfile("source_file", file: file) do |source_file| + File.write(source_file, source) + compile_file(source_file, flags, file: file) do |executable_file| + yield executable_file + end + end +end + +def compile_and_run_file(source_file, flags = %w(), file = __FILE__) + compile_file(source_file, flags, file: file) do |executable_file| + output, error = IO::Memory.new, IO::Memory.new + status = Process.run executable_file, output: output, error: error + + {status, output.to_s, error.to_s} + end +end + +def compile_and_run_source(source, flags = %w(), file = __FILE__) + with_tempfile("source_file", file: file) do |source_file| + File.write(source_file, source) + compile_and_run_file(source_file, flags, file: file) + end +end + +def compile_and_run_source_with_c(c_code, crystal_code, flags = %w(--debug), file = __FILE__) + with_temp_c_object_file(c_code, file: file) do |o_filename| + yield compile_and_run_source(%( + require "prelude" + + @[Link(ldflags: #{o_filename.inspect})] + #{crystal_code} + )) + end +end diff --git a/spec/std/string/utf16_spec.cr b/spec/std/string/utf16_spec.cr index ef1b78be1bcf..912772ea050b 100644 --- a/spec/std/string/utf16_spec.cr +++ b/spec/std/string/utf16_spec.cr @@ -28,7 +28,7 @@ describe "String UTF16" do it "in the range U+D800..U+DFFF" do encoded = "\u{D800}\u{DFFF}".to_utf16 - encoded.should eq(Slice[0xFFFD_u16, 0xFFFD_u16]) + encoded.should eq(Slice[0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16]) encoded.unsafe_fetch(encoded.size).should eq 0_u16 end end diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr index 029d149ed9a2..9ba90a5b0284 100644 --- a/spec/std/string_spec.cr +++ b/spec/std/string_spec.cr @@ -97,7 +97,7 @@ describe "String" do end it "gets with start and count with negative start" do - "こんいちは"[-3, 2].should eq("いち") + "こんにちは"[-3, 2].should eq("にち") end it "raises if index out of bounds" do @@ -108,7 +108,7 @@ describe "String" do it "raises if index out of bounds with utf-8" do expect_raises(IndexError) do - "こんいちは"[6, 1] + "こんにちは"[6, 1] end end @@ -120,7 +120,7 @@ describe "String" do it "raises if count is negative with utf-8" do expect_raises(ArgumentError) do - "こんいちは"[3, -1] + "こんにちは"[3, -1] end end @@ -530,7 +530,7 @@ describe "String" do end end - describe "downcase" do + describe "#downcase" do it { "HELLO!".downcase.should eq("hello!") } it { "HELLO MAN!".downcase.should eq("hello man!") } it { "ÁÉÍÓÚĀ".downcase.should eq("áéíóúā") } @@ -541,9 +541,22 @@ describe "String" do it { "ff".downcase(Unicode::CaseOptions::Fold).should eq("ff") } it { "tschüß".downcase(Unicode::CaseOptions::Fold).should eq("tschüss") } it { "ΣίσυφοςfiÆ".downcase(Unicode::CaseOptions::Fold).should eq("σίσυφοσfiæ") } + + describe "with IO" do + it { String.build { |io| "HELLO!".downcase io }.should eq "hello!" } + it { String.build { |io| "HELLO MAN!".downcase io }.should eq "hello man!" } + it { String.build { |io| "ÁÉÍÓÚĀ".downcase io }.should eq "áéíóúā" } + it { String.build { |io| "AEIİOU".downcase io, Unicode::CaseOptions::Turkic }.should eq "aeıiou" } + it { String.build { |io| "ÁEÍOÚ".downcase io, Unicode::CaseOptions::ASCII }.should eq "ÁeÍoÚ" } + it { String.build { |io| "İ".downcase io }.should eq "i̇" } + it { String.build { |io| "Baffle".downcase io, Unicode::CaseOptions::Fold }.should eq "baffle" } + it { String.build { |io| "ff".downcase io, Unicode::CaseOptions::Fold }.should eq "ff" } + it { String.build { |io| "tschüß".downcase io, Unicode::CaseOptions::Fold }.should eq "tschüss" } + it { String.build { |io| "ΣίσυφοςfiÆ".downcase io, Unicode::CaseOptions::Fold }.should eq "σίσυφοσfiæ" } + end end - describe "upcase" do + describe "#upcase" do it { "hello!".upcase.should eq("HELLO!") } it { "hello man!".upcase.should eq("HELLO MAN!") } it { "áéíóúā".upcase.should eq("ÁÉÍÓÚĀ") } @@ -553,17 +566,37 @@ describe "String" do it { "baffle".upcase.should eq("BAFFLE") } it { "ff".upcase.should eq("FF") } it { "ňž".upcase.should eq("ŇŽ") } # #7922 + + describe "with IO" do + it { String.build { |io| "hello!".upcase io }.should eq "HELLO!" } + it { String.build { |io| "hello man!".upcase io }.should eq "HELLO MAN!" } + it { String.build { |io| "áéíóúā".upcase io }.should eq "ÁÉÍÓÚĀ" } + it { String.build { |io| "aeıiou".upcase io, Unicode::CaseOptions::Turkic }.should eq "AEIİOU" } + it { String.build { |io| "áeíoú".upcase io, Unicode::CaseOptions::ASCII }.should eq "áEíOú" } + it { String.build { |io| "aeiou".upcase io, Unicode::CaseOptions::Turkic }.should eq "AEİOU" } + it { String.build { |io| "baffle".upcase io }.should eq "BAFFLE" } + it { String.build { |io| "ff".upcase io }.should eq "FF" } + it { String.build { |io| "ňž".upcase io }.should eq "ŇŽ" } + end end - describe "capitalize" do + describe "#capitalize" do it { "HELLO!".capitalize.should eq("Hello!") } it { "HELLO MAN!".capitalize.should eq("Hello man!") } it { "".capitalize.should eq("") } it { "fflİ".capitalize.should eq("FFLi̇") } it { "iO".capitalize(Unicode::CaseOptions::Turkic).should eq("İo") } + + describe "with IO" do + it { String.build { |io| "HELLO!".capitalize io }.should eq "Hello!" } + it { String.build { |io| "HELLO MAN!".capitalize io }.should eq "Hello man!" } + it { String.build { |io| "".capitalize io }.should be_empty } + it { String.build { |io| "fflİ".capitalize io }.should eq "FFLi̇" } + it { String.build { |io| "iO".capitalize io, Unicode::CaseOptions::Turkic }.should eq "İo" } + end end - describe "titleize" do + describe "#titleize" do it { "hEllO tAb\tworld".titleize.should eq("Hello Tab\tWorld") } it { " spaces before".titleize.should eq(" Spaces Before") } it { "testa-se muito".titleize.should eq("Testa-se Muito") } @@ -571,6 +604,16 @@ describe "String" do it { " spáçes before".titleize.should eq(" Spáçes Before") } it { "testá-se múitô".titleize.should eq("Testá-se Múitô") } it { "iO iO".titleize(Unicode::CaseOptions::Turkic).should eq("İo İo") } + + describe "with IO" do + it { String.build { |io| "hEllO tAb\tworld".titleize io }.should eq "Hello Tab\tWorld" } + it { String.build { |io| " spaces before".titleize io }.should eq " Spaces Before" } + it { String.build { |io| "testa-se muito".titleize io }.should eq "Testa-se Muito" } + it { String.build { |io| "hÉllÕ tAb\tworld".titleize io }.should eq "Héllõ Tab\tWorld" } + it { String.build { |io| " spáçes before".titleize io }.should eq " Spáçes Before" } + it { String.build { |io| "testá-se múitô".titleize io }.should eq "Testá-se Múitô" } + it { String.build { |io| "iO iO".titleize io, Unicode::CaseOptions::Turkic }.should eq "İo İo" } + end end describe "chomp" do @@ -656,6 +699,7 @@ describe "String" do describe "rchop?" do it { "".rchop?.should be_nil } + it { "\n".rchop?.should eq("") } it { "foo".rchop?.should eq("fo") } it { "foo\n".rchop?.should eq("foo") } it { "foo\r".rchop?.should eq("foo") } @@ -788,6 +832,8 @@ describe "String" do it { "foo".index("").should eq(0) } it { "foo".index("foo").should eq(0) } it { "日本語日本語".index("本語").should eq(1) } + it { "\xFF\xFFcrystal".index("crystal").should eq(2) } + it { "\xFD\x9A\xAD\x50NG".index("PNG").should eq(3) } describe "with offset" do it { "foobarbaz".index("ba", 4).should eq(6) } @@ -797,6 +843,8 @@ describe "String" do it { "foo".index("", 3).should eq(3) } it { "foo".index("", 4).should be_nil } it { "日本語日本語".index("本語", 2).should eq(4) } + it { "\xFD\x9A\xAD\x50NG".index("PNG", 2).should eq(3) } + it { "\xFD\x9A\xAD\x50NG".index("PNG", 4).should be_nil } end end @@ -867,6 +915,12 @@ describe "String" do it { "a43b53".rindex(/\d+/).should eq(4) } it { "bbbb".rindex(/\d/).should be_nil } + describe "which matches empty string" do + it { "foo".rindex(/o*/).should eq(3) } + it { "foo".rindex(//).should eq(3) } + it { "foo".rindex(/\b/).should eq(3) } + end + describe "with offset" do it { "bbbb".rindex(/b/, 2).should eq(2) } it { "abbbb".rindex(/b/, 0).should be_nil } @@ -881,57 +935,57 @@ describe "String" do describe "partition" do describe "by char" do - "hello".partition('h').should eq ({"", "h", "ello"}) - "hello".partition('o').should eq ({"hell", "o", ""}) - "hello".partition('l').should eq ({"he", "l", "lo"}) - "hello".partition('x').should eq ({"hello", "", ""}) + it { "hello".partition('h').should eq ({"", "h", "ello"}) } + it { "hello".partition('o').should eq ({"hell", "o", ""}) } + it { "hello".partition('l').should eq ({"he", "l", "lo"}) } + it { "hello".partition('x').should eq ({"hello", "", ""}) } end describe "by string" do - "hello".partition("h").should eq ({"", "h", "ello"}) - "hello".partition("o").should eq ({"hell", "o", ""}) - "hello".partition("l").should eq ({"he", "l", "lo"}) - "hello".partition("ll").should eq ({"he", "ll", "o"}) - "hello".partition("x").should eq ({"hello", "", ""}) + it { "hello".partition("h").should eq ({"", "h", "ello"}) } + it { "hello".partition("o").should eq ({"hell", "o", ""}) } + it { "hello".partition("l").should eq ({"he", "l", "lo"}) } + it { "hello".partition("ll").should eq ({"he", "ll", "o"}) } + it { "hello".partition("x").should eq ({"hello", "", ""}) } end describe "by regex" do - "hello".partition(/h/).should eq ({"", "h", "ello"}) - "hello".partition(/o/).should eq ({"hell", "o", ""}) - "hello".partition(/l/).should eq ({"he", "l", "lo"}) - "hello".partition(/ll/).should eq ({"he", "ll", "o"}) - "hello".partition(/.l/).should eq ({"h", "el", "lo"}) - "hello".partition(/.h/).should eq ({"hello", "", ""}) - "hello".partition(/h./).should eq ({"", "he", "llo"}) - "hello".partition(/o./).should eq ({"hello", "", ""}) - "hello".partition(/.o/).should eq ({"hel", "lo", ""}) - "hello".partition(/x/).should eq ({"hello", "", ""}) + it { "hello".partition(/h/).should eq ({"", "h", "ello"}) } + it { "hello".partition(/o/).should eq ({"hell", "o", ""}) } + it { "hello".partition(/l/).should eq ({"he", "l", "lo"}) } + it { "hello".partition(/ll/).should eq ({"he", "ll", "o"}) } + it { "hello".partition(/.l/).should eq ({"h", "el", "lo"}) } + it { "hello".partition(/.h/).should eq ({"hello", "", ""}) } + it { "hello".partition(/h./).should eq ({"", "he", "llo"}) } + it { "hello".partition(/o./).should eq ({"hello", "", ""}) } + it { "hello".partition(/.o/).should eq ({"hel", "lo", ""}) } + it { "hello".partition(/x/).should eq ({"hello", "", ""}) } end end describe "rpartition" do describe "by char" do - "hello".rpartition('l').should eq ({"hel", "l", "o"}) - "hello".rpartition('o').should eq ({"hell", "o", ""}) - "hello".rpartition('h').should eq ({"", "h", "ello"}) + it { "hello".rpartition('l').should eq ({"hel", "l", "o"}) } + it { "hello".rpartition('o').should eq ({"hell", "o", ""}) } + it { "hello".rpartition('h').should eq ({"", "h", "ello"}) } end describe "by string" do - "hello".rpartition("l").should eq ({"hel", "l", "o"}) - "hello".rpartition("x").should eq ({"", "", "hello"}) - "hello".rpartition("o").should eq ({"hell", "o", ""}) - "hello".rpartition("h").should eq ({"", "h", "ello"}) - "hello".rpartition("ll").should eq ({"he", "ll", "o"}) - "hello".rpartition("lo").should eq ({"hel", "lo", ""}) - "hello".rpartition("he").should eq ({"", "he", "llo"}) + it { "hello".rpartition("l").should eq ({"hel", "l", "o"}) } + it { "hello".rpartition("x").should eq ({"", "", "hello"}) } + it { "hello".rpartition("o").should eq ({"hell", "o", ""}) } + it { "hello".rpartition("h").should eq ({"", "h", "ello"}) } + it { "hello".rpartition("ll").should eq ({"he", "ll", "o"}) } + it { "hello".rpartition("lo").should eq ({"hel", "lo", ""}) } + it { "hello".rpartition("he").should eq ({"", "he", "llo"}) } end describe "by regex" do - "hello".rpartition(/.l/).should eq ({"he", "ll", "o"}) - "hello".rpartition(/ll/).should eq ({"he", "ll", "o"}) - "hello".rpartition(/.o/).should eq ({"hel", "lo", ""}) - "hello".rpartition(/.e/).should eq ({"", "he", "llo"}) - "hello".rpartition(/l./).should eq ({"hel", "lo", ""}) + it { "hello".rpartition(/.l/).should eq ({"he", "ll", "o"}) } + it { "hello".rpartition(/ll/).should eq ({"he", "ll", "o"}) } + it { "hello".rpartition(/.o/).should eq ({"hel", "lo", ""}) } + it { "hello".rpartition(/.e/).should eq ({"", "he", "llo"}) } + it { "hello".rpartition(/l./).should eq ({"hel", "lo", ""}) } end end @@ -1128,7 +1182,7 @@ describe "String" do end it "reverses utf-8 string" do - "こんいちは".reverse.should eq("はちいんこ") + "こんにちは".reverse.should eq("はちにんこ") end it "reverses taking grapheme clusters into account" do @@ -1668,17 +1722,31 @@ describe "String" do ("%+i" % -123).should eq("-123") ("% i" % 123).should eq(" 123") ("%20d" % 123).should eq(" 123") + ("%20d" % -123).should eq(" -123") + ("%20d" % 0).should eq(" 0") ("%+20d" % 123).should eq(" +123") ("%+20d" % -123).should eq(" -123") + ("%+20d" % 0).should eq(" +0") ("% 20d" % 123).should eq(" 123") ("%020d" % 123).should eq("00000000000000000123") + ("%020d" % -123).should eq("0000000000000000-123") + ("%020d" % 0).should eq("00000000000000000000") ("%+020d" % 123).should eq("+0000000000000000123") + ("%+020d" % -123).should eq("-0000000000000000123") + ("%+020d" % 0).should eq("+0000000000000000000") ("% 020d" % 123).should eq(" 0000000000000000123") + ("% 020d" % 0).should eq(" 0000000000000000000") ("%-d" % 123).should eq("123") + ("%-d" % 0).should eq("0") ("%-20d" % 123).should eq("123 ") + ("%-20d" % -123).should eq("-123 ") + ("%-20d" % 0).should eq("0 ") ("%-+20d" % 123).should eq("+123 ") ("%-+20d" % -123).should eq("-123 ") + ("%-+20d" % 0).should eq("+0 ") ("%- 20d" % 123).should eq(" 123 ") + ("%- 20d" % -123).should eq("-123 ") + ("%- 20d" % 0).should eq(" 0 ") ("%s" % 'a').should eq("a") ("%-s" % 'a').should eq("a") ("%20s" % 'a').should eq(" a") @@ -1808,16 +1876,36 @@ describe "String" do "ぜんぶ".chars.should eq(['ぜ', 'ん', 'ぶ']) end - it "allows creating a string with zeros" do - p = Pointer(UInt8).malloc(3) - p[0] = 'a'.ord.to_u8 - p[1] = '\0'.ord.to_u8 - p[2] = 'b'.ord.to_u8 - s = String.new(p, 3) - s[0].should eq('a') - s[1].should eq('\0') - s[2].should eq('b') - s.bytesize.should eq(3) + describe "creating from a pointer" do + it "allows creating a string with zeros" do + p = Pointer(UInt8).malloc(3) + p[0] = 'a'.ord.to_u8 + p[1] = '\0'.ord.to_u8 + p[2] = 'b'.ord.to_u8 + s = String.new(p, 3) + s[0].should eq('a') + s[1].should eq('\0') + s[2].should eq('b') + s.bytesize.should eq(3) + end + + it "raises an exception when creating a string with a null pointer and no size" do + expect_raises ArgumentError do + String.new(Pointer(UInt8).null) + end + end + + it "raises when creating from a null pointer with a nonzero size" do + expect_raises ArgumentError do + String.new(Pointer(UInt8).null, 3) + end + end + + it "raises when creating from a null pointer with size 0" do + expect_raises ArgumentError do + String.new(Pointer(UInt8).null, 0).should eq "" + end + end end describe "tr" do @@ -1869,53 +1957,85 @@ describe "String" do end end - it "does underscore" do - "Foo".underscore.should eq("foo") - "FooBar".underscore.should eq("foo_bar") - "ABCde".underscore.should eq("ab_cde") - "FOO_bar".underscore.should eq("foo_bar") - "Char_S".underscore.should eq("char_s") - "Char_".underscore.should eq("char_") - "C_".underscore.should eq("c_") - "HTTP".underscore.should eq("http") - "HTTP_CLIENT".underscore.should eq("http_client") - "CSS3".underscore.should eq("css3") - "HTTP1.1".underscore.should eq("http1.1") - "3.14IsPi".underscore.should eq("3.14_is_pi") - "I2C".underscore.should eq("i2_c") - end - - it "does camelcase" do - "foo".camelcase.should eq("Foo") - "foo_bar".camelcase.should eq("FooBar") - "foo".camelcase(lower: true).should eq("foo") - "foo_bar".camelcase(lower: true).should eq("fooBar") - - "Foo".camelcase.should eq("Foo") - "Foo_bar".camelcase.should eq("FooBar") - "Foo".camelcase(lower: true).should eq("foo") - "Foo_bar".camelcase(lower: true).should eq("fooBar") - end - - it "answers ascii_only?" do - "a".ascii_only?.should be_true - "あ".ascii_only?.should be_false + describe "#underscore" do + it { "Foo".underscore.should eq "foo" } + it { "FooBar".underscore.should eq "foo_bar" } + it { "ABCde".underscore.should eq "ab_cde" } + it { "FOO_bar".underscore.should eq "foo_bar" } + it { "Char_S".underscore.should eq "char_s" } + it { "Char_".underscore.should eq "char_" } + it { "C_".underscore.should eq "c_" } + it { "HTTP".underscore.should eq "http" } + it { "HTTP_CLIENT".underscore.should eq "http_client" } + it { "CSS3".underscore.should eq "css3" } + it { "HTTP1.1".underscore.should eq "http1.1" } + it { "3.14IsPi".underscore.should eq "3.14_is_pi" } + it { "I2C".underscore.should eq "i2_c" } + + describe "with IO" do + it { String.build { |io| "Foo".underscore io }.should eq "foo" } + it { String.build { |io| "FooBar".underscore io }.should eq "foo_bar" } + it { String.build { |io| "ABCde".underscore io }.should eq "ab_cde" } + it { String.build { |io| "FOO_bar".underscore io }.should eq "foo_bar" } + it { String.build { |io| "Char_S".underscore io }.should eq "char_s" } + it { String.build { |io| "Char_".underscore io }.should eq "char_" } + it { String.build { |io| "C_".underscore io }.should eq "c_" } + it { String.build { |io| "HTTP".underscore io }.should eq "http" } + it { String.build { |io| "HTTP_CLIENT".underscore io }.should eq "http_client" } + it { String.build { |io| "CSS3".underscore io }.should eq "css3" } + it { String.build { |io| "HTTP1.1".underscore io }.should eq "http1.1" } + it { String.build { |io| "3.14IsPi".underscore io }.should eq "3.14_is_pi" } + it { String.build { |io| "I2C".underscore io }.should eq "i2_c" } + end + end + + describe "#camelcase" do + it { "foo".camelcase.should eq "Foo" } + it { "foo_bar".camelcase.should eq "FooBar" } + it { "foo".camelcase(lower: true).should eq "foo" } + it { "foo_bar".camelcase(lower: true).should eq "fooBar" } + it { "Foo".camelcase.should eq "Foo" } + it { "Foo_bar".camelcase.should eq "FooBar" } + it { "Foo".camelcase(lower: true).should eq "foo" } + it { "Foo_bar".camelcase(lower: true).should eq "fooBar" } + + describe "with IO" do + it { String.build { |io| "foo".camelcase io }.should eq "Foo" } + it { String.build { |io| "foo_bar".camelcase io }.should eq "FooBar" } + it { String.build { |io| "foo".camelcase io, lower: true }.should eq "foo" } + it { String.build { |io| "foo_bar".camelcase io, lower: true }.should eq "fooBar" } + it { String.build { |io| "Foo".camelcase io }.should eq "Foo" } + it { String.build { |io| "Foo_bar".camelcase io }.should eq "FooBar" } + it { String.build { |io| "Foo".camelcase io, lower: true }.should eq "foo" } + it { String.build { |io| "Foo_bar".camelcase io, lower: true }.should eq "fooBar" } + end + end + + describe "ascii_only?" do + it "answers ascii_only?" do + "a".ascii_only?.should be_true + "あ".ascii_only?.should be_false + + str = String.new(1) do |buffer| + buffer.value = 'a'.ord.to_u8 + {1, 0} + end + str.ascii_only?.should be_true - str = String.new(1) do |buffer| - buffer.value = 'a'.ord.to_u8 - {1, 0} + str = String.new(4) do |buffer| + count = 0 + 'あ'.each_byte do |byte| + buffer[count] = byte + count += 1 + end + {count, 0} + end + str.ascii_only?.should be_false end - str.ascii_only?.should be_true - str = String.new(4) do |buffer| - count = 0 - 'あ'.each_byte do |byte| - buffer[count] = byte - count += 1 - end - {count, 0} + it "broken UTF-8 is not ascii_only" do + "\xED\xA0\x80\xED\xBF\xBF".ascii_only?.should be_false end - str.ascii_only?.should be_false end describe "scan" do @@ -2031,10 +2151,17 @@ describe "String" do it { "12".ljust(7, 'あ').should eq("12あああああ") } describe "to io" do - it { with_io_memory { |io| "123".ljust(2, io) }.should eq("123") } - it { with_io_memory { |io| "123".ljust(5, io) }.should eq("123 ") } - it { with_io_memory { |io| "12".ljust(7, '-', io) }.should eq("12-----") } - it { with_io_memory { |io| "12".ljust(7, 'あ', io) }.should eq("12あああああ") } + it { String.build { |io| "123".ljust(io, 2) }.should eq("123") } + it { String.build { |io| "123".ljust(io, 5) }.should eq("123 ") } + it { String.build { |io| "12".ljust(io, 7, '-') }.should eq("12-----") } + it { String.build { |io| "12".ljust(io, 7, 'あ') }.should eq("12あああああ") } + end + + describe "to io (deprecated)" do + it { String.build { |io| "123".ljust(2, io) }.should eq("123") } + it { String.build { |io| "123".ljust(5, io) }.should eq("123 ") } + it { String.build { |io| "12".ljust(7, '-', io) }.should eq("12-----") } + it { String.build { |io| "12".ljust(7, 'あ', io) }.should eq("12あああああ") } end end @@ -2045,10 +2172,17 @@ describe "String" do it { "12".rjust(7, 'あ').should eq("あああああ12") } describe "to io" do - it { with_io_memory { |io| "123".rjust(2, io) }.should eq("123") } - it { with_io_memory { |io| "123".rjust(5, io) }.should eq(" 123") } - it { with_io_memory { |io| "12".rjust(7, '-', io) }.should eq("-----12") } - it { with_io_memory { |io| "12".rjust(7, 'あ', io) }.should eq("あああああ12") } + it { String.build { |io| "123".rjust(io, 2) }.should eq("123") } + it { String.build { |io| "123".rjust(io, 5) }.should eq(" 123") } + it { String.build { |io| "12".rjust(io, 7, '-') }.should eq("-----12") } + it { String.build { |io| "12".rjust(io, 7, 'あ') }.should eq("あああああ12") } + end + + describe "to io (deprecated)" do + it { String.build { |io| "123".rjust(2, io) }.should eq("123") } + it { String.build { |io| "123".rjust(5, io) }.should eq(" 123") } + it { String.build { |io| "12".rjust(7, '-', io) }.should eq("-----12") } + it { String.build { |io| "12".rjust(7, 'あ', io) }.should eq("あああああ12") } end end @@ -2059,10 +2193,17 @@ describe "String" do it { "12".center(7, 'あ').should eq("ああ12あああ") } describe "to io" do - it { with_io_memory { |io| "123".center(2, io) }.should eq("123") } - it { with_io_memory { |io| "123".center(5, io) }.should eq(" 123 ") } - it { with_io_memory { |io| "12".center(7, '-', io) }.should eq("--12---") } - it { with_io_memory { |io| "12".center(7, 'あ', io) }.should eq("ああ12あああ") } + it { String.build { |io| "123".center(io, 2) }.should eq("123") } + it { String.build { |io| "123".center(io, 5) }.should eq(" 123 ") } + it { String.build { |io| "12".center(io, 7, '-') }.should eq("--12---") } + it { String.build { |io| "12".center(io, 7, 'あ') }.should eq("ああ12あああ") } + end + + describe "to io (deprecated)" do + it { String.build { |io| "123".center(2, io) }.should eq("123") } + it { String.build { |io| "123".center(5, io) }.should eq(" 123 ") } + it { String.build { |io| "12".center(7, '-', io) }.should eq("--12---") } + it { String.build { |io| "12".center(7, 'あ', io) }.should eq("ああ12あああ") } end end @@ -2357,7 +2498,7 @@ describe "String" do end end - pending_win32 "applies formatting to %<...> placeholder" do + it "applies formatting to %<...> placeholder" do res = "change %.2f" % {"this" => 23.456} res.should eq "change 23.46" @@ -2599,9 +2740,9 @@ describe "String" do it "scrubs" do string = String.new(Bytes[255, 129, 97, 255, 97]) - string.scrub.bytes.should eq([239, 191, 189, 97, 239, 191, 189, 97]) + string.scrub.bytes.should eq([239, 191, 189, 239, 191, 189, 97, 239, 191, 189, 97]) - string.scrub("?").should eq("?a?a") + string.scrub("?").should eq("??a?a") "hello".scrub.should eq("hello") end @@ -2634,10 +2775,69 @@ describe "String" do String.interpolation("a", 123, "b", 456, "cde").should eq("a123b456cde") end end -end -private def with_io_memory - io = IO::Memory.new - yield io - io.to_s + describe "delete_at" do + describe "char" do + it { "abcde".delete_at(0).should eq("bcde") } + it { "abcde".delete_at(1).should eq("acde") } + it { "abcde".delete_at(2).should eq("abde") } + it { "abcde".delete_at(4).should eq("abcd") } + it { "abcde".delete_at(-2).should eq("abce") } + it { expect_raises(IndexError) { "abcde".delete_at(5) } } + it { expect_raises(IndexError) { "abcde".delete_at(-6) } } + + it { "二ノ国".delete_at(0).should eq("ノ国") } + it { "二ノ国".delete_at(1).should eq("二国") } + it { "二ノ国".delete_at(2).should eq("二ノ") } + it { "二ノ国".delete_at(-2).should eq("二国") } + it { expect_raises(IndexError) { "二ノ国".delete_at(3) } } + it { expect_raises(IndexError) { "二ノ国".delete_at(-4) } } + end + + describe "start, count" do + it { "abcdefg".delete_at(0, 2).should eq("cdefg") } + it { "abcdefg".delete_at(1, 2).should eq("adefg") } + it { "abcdefg".delete_at(3, 10).should eq("abc") } + it { "abcdefg".delete_at(-3, 2).should eq("abcdg") } + it { "abcdefg".delete_at(7, 10).should eq("abcdefg") } + it { expect_raises(IndexError) { "abcdefg".delete_at(8, 1) } } + it { expect_raises(IndexError) { "abcdefg".delete_at(-8, 1) } } + + it "raises on negative count" do + expect_raises(ArgumentError, "Negative count: -1") { + "abcdefg".delete_at(1, -1) + } + end + + it { "セキロ:シャドウズ ダイ トゥワイス".delete_at(4, 6).should eq("セキロ:ダイ トゥワイス") } + it { "セキロ:シャドウズ ダイ トゥワイス".delete_at(0, 4).should eq("シャドウズ ダイ トゥワイス") } + it { "セキロ:シャドウズ ダイ トゥワイス".delete_at(3, 20).should eq("セキロ") } + it { "セキロ:シャドウズ ダイ トゥワイス".delete_at(-14, 6).should eq("セキロ:ダイ トゥワイス") } + it { "セキロ:シャドウズ ダイ トゥワイス".delete_at(18, 3).should eq("セキロ:シャドウズ ダイ トゥワイス") } + it { expect_raises(IndexError) { "セキロ:シャドウズ ダイ トゥワイス".delete_at(19, 1) } } + it { expect_raises(IndexError) { "セキロ:シャドウズ ダイ トゥワイス".delete_at(-19, 1) } } + + it "raises on negative count" do + expect_raises(ArgumentError, "Negative count: -1") { + "セキロ:シャドウズ ダイ トゥワイス".delete_at(1, -1) + } + end + end + + describe "range" do + it { "abcdefg".delete_at(0..1).should eq("cdefg") } + it { "abcdefg".delete_at(0...2).should eq("cdefg") } + it { "abcdefg".delete_at(1..3).should eq("aefg") } + it { "abcdefg".delete_at(3..10).should eq("abc") } + it { "abcdefg".delete_at(-3..-2).should eq("abcdg") } + it { "abcdefg".delete_at(3..).should eq("abc") } + it { "abcdefg".delete_at(..-3).should eq("fg") } + it { expect_raises(IndexError) { "abcdefg".delete_at(8..1) } } + it { expect_raises(IndexError) { "abcdefg".delete_at(-8..1) } } + + it { "セキロ:シャドウズ ダイ トゥワイス".delete_at(4...10).should eq("セキロ:ダイ トゥワイス") } + it { expect_raises(IndexError) { "セキロ:シャドウズ ダイ トゥワイス".delete_at(19..1) } } + it { expect_raises(IndexError) { "セキロ:シャドウズ ダイ トゥワイス".delete_at(-19..1) } } + end + end end diff --git a/spec/std/time/format_spec.cr b/spec/std/time/format_spec.cr index 44926a37f907..03fd3362d24d 100644 --- a/spec/std/time/format_spec.cr +++ b/spec/std/time/format_spec.cr @@ -153,6 +153,24 @@ describe Time::Format do Time.parse_rfc2822(time.to_rfc2822).should eq time end + it "formats rfc3339 with different fraction digits" do + time = Time.utc(2016, 2, 15, 8, 23, 45, nanosecond: 123456789) + time.to_rfc3339.should eq "2016-02-15T08:23:45Z" + time.to_rfc3339(fraction_digits: 0).should eq "2016-02-15T08:23:45Z" + time.to_rfc3339(fraction_digits: 3).should eq "2016-02-15T08:23:45.123Z" + time.to_rfc3339(fraction_digits: 6).should eq "2016-02-15T08:23:45.123456Z" + time.to_rfc3339(fraction_digits: 9).should eq "2016-02-15T08:23:45.123456789Z" + expect_raises(ArgumentError, "Invalid fraction digits: 5") { time.to_rfc3339(fraction_digits: 5) } + expect_raises(ArgumentError, "Invalid fraction digits: -1") { time.to_rfc3339(fraction_digits: -1) } + + time = Time.utc(2016, 2, 15, 8, 23, 45) + time.to_rfc3339.should eq "2016-02-15T08:23:45Z" + time.to_rfc3339(fraction_digits: 0).should eq "2016-02-15T08:23:45Z" + time.to_rfc3339(fraction_digits: 3).should eq "2016-02-15T08:23:45.000Z" + time.to_rfc3339(fraction_digits: 6).should eq "2016-02-15T08:23:45.000000Z" + time.to_rfc3339(fraction_digits: 9).should eq "2016-02-15T08:23:45.000000000Z" + end + it "parses empty" do t = Time.parse("", "", Time::Location.local) t.year.should eq(1) @@ -200,6 +218,10 @@ describe Time::Format do parse_time(" 9", "%l").hour.should eq(9) parse_time("9pm", "%l%p").hour.should eq(21) parse_time("9PM", "%l%P").hour.should eq(21) + parse_time("12PM", "%l%P").hour.should eq(12) + parse_time("9am", "%l%p").hour.should eq(9) + parse_time("9AM", "%l%P").hour.should eq(9) + parse_time("12AM", "%l%P").hour.should eq(0) parse_time("09", "%M").minute.should eq(9) parse_time("09", "%S").second.should eq(9) parse_time("123", "%L").millisecond.should eq(123) @@ -249,6 +271,148 @@ describe Time::Format do parse_time("2009-W53-7", "%G-W%V-%u").should eq(Time.utc(2010, 1, 3)) end + it "parses am/pm" do + parse_time("12:00 am", "%I:%M %P").to_s("%H:%M").should eq("00:00") + parse_time("12:01 am", "%I:%M %P").to_s("%H:%M").should eq("00:01") + parse_time("01:00 am", "%I:%M %P").to_s("%H:%M").should eq("01:00") + parse_time("11:00 am", "%I:%M %P").to_s("%H:%M").should eq("11:00") + parse_time("00:00 pm", "%I:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("00:01 pm", "%I:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("12:00 pm", "%I:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("12:01 pm", "%I:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("01:00 pm", "%I:%M %P").to_s("%H:%M").should eq("13:00") + parse_time("11:00 pm", "%I:%M %P").to_s("%H:%M").should eq("23:00") + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("00:00 am", "%I:%M %P") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("00:01 am", "%I:%M %P") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("13:00 am", "%I:%M %P") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("13:00 pm", "%I:%M %P") + end + + parse_time("12:00", "%I:%M").to_s("%H:%M").should eq("00:00") + parse_time("12:01", "%I:%M").to_s("%H:%M").should eq("00:01") + parse_time("01:00", "%I:%M").to_s("%H:%M").should eq("01:00") + parse_time("11:00", "%I:%M").to_s("%H:%M").should eq("11:00") + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("00:00", "%I:%M") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("00:01", "%I:%M") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("13:00", "%I:%M") + end + + parse_time("12:00 am", "%l:%M %P").to_s("%H:%M").should eq("00:00") + parse_time("12:01 am", "%l:%M %P").to_s("%H:%M").should eq("00:01") + parse_time(" 1:00 am", "%l:%M %P").to_s("%H:%M").should eq("01:00") + parse_time("11:00 am", "%l:%M %P").to_s("%H:%M").should eq("11:00") + parse_time(" 0:00 pm", "%l:%M %P").to_s("%H:%M").should eq("12:00") + parse_time(" 0:01 pm", "%l:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("12:00 pm", "%l:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("12:01 pm", "%l:%M %P").to_s("%H:%M").should eq("12:01") + parse_time(" 1:00 pm", "%l:%M %P").to_s("%H:%M").should eq("13:00") + parse_time("11:00 pm", "%l:%M %P").to_s("%H:%M").should eq("23:00") + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time(" 0:00 am", "%l:%M %P") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time(" 0:01 am", "%l:%M %P") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("13:00 am", "%l:%M %P") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("13:00 pm", "%l:%M %P") + end + + parse_time("12:00", "%l:%M").to_s("%H:%M").should eq("00:00") + parse_time("12:01", "%l:%M").to_s("%H:%M").should eq("00:01") + parse_time("01:00", "%l:%M").to_s("%H:%M").should eq("01:00") + parse_time("11:00", "%l:%M").to_s("%H:%M").should eq("11:00") + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time(" 0:00", "%l:%M") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time(" 0:01", "%l:%M") + end + expect_raises Time::Format::Error, "Invalid hour for 12-hour clock" do + parse_time("13:00", "%l:%M") + end + end + + it "parses 24h clock" do + parse_time("00:00", "%H:%M").to_s("%H:%M").should eq("00:00") + parse_time("00:01", "%H:%M").to_s("%H:%M").should eq("00:01") + parse_time("01:00", "%H:%M").to_s("%H:%M").should eq("01:00") + parse_time("11:00", "%H:%M").to_s("%H:%M").should eq("11:00") + parse_time("12:00", "%H:%M").to_s("%H:%M").should eq("12:00") + parse_time("12:01", "%H:%M").to_s("%H:%M").should eq("12:01") + parse_time("13:00", "%H:%M").to_s("%H:%M").should eq("13:00") + parse_time("23:00", "%H:%M").to_s("%H:%M").should eq("23:00") + parse_time("24:00", "%H:%M").to_s("%H:%M").should eq("00:00") + parse_time("2020-05-21 24:00", "%F %H:%M").should eq(Time.utc(2020, 5, 22, 0, 0)) + + parse_time(" 0:00", "%k:%M").to_s("%H:%M").should eq("00:00") + parse_time(" 0:01", "%k:%M").to_s("%H:%M").should eq("00:01") + parse_time(" 1:00", "%k:%M").to_s("%H:%M").should eq("01:00") + parse_time("11:00", "%k:%M").to_s("%H:%M").should eq("11:00") + parse_time("12:00", "%k:%M").to_s("%H:%M").should eq("12:00") + parse_time("12:01", "%k:%M").to_s("%H:%M").should eq("12:01") + parse_time("13:00", "%k:%M").to_s("%H:%M").should eq("13:00") + parse_time("23:00", "%k:%M").to_s("%H:%M").should eq("23:00") + parse_time("24:00", "%k:%M").to_s("%H:%M").should eq("00:00") + parse_time("2020-05-21 24:00", "%F %k:%M").should eq(Time.utc(2020, 5, 22, 0, 0)) + end + + it "parses 24h clock with am/pm" do + parse_time("00:00 AM", "%H:%M %P").to_s("%H:%M").should eq("00:00") + parse_time("00:01 AM", "%H:%M %P").to_s("%H:%M").should eq("00:01") + parse_time("01:00 AM", "%H:%M %P").to_s("%H:%M").should eq("01:00") + parse_time("11:00 AM", "%H:%M %P").to_s("%H:%M").should eq("11:00") + parse_time("12:00 AM", "%H:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("12:01 AM", "%H:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("13:00 AM", "%H:%M %P").to_s("%H:%M").should eq("13:00") + parse_time("23:00 AM", "%H:%M %P").to_s("%H:%M").should eq("23:00") + parse_time("24:00 AM", "%H:%M %P").to_s("%H:%M").should eq("00:00") + + parse_time(" 0:00 AM", "%k:%M %P").to_s("%H:%M").should eq("00:00") + parse_time(" 0:01 AM", "%k:%M %P").to_s("%H:%M").should eq("00:01") + parse_time(" 1:00 AM", "%k:%M %P").to_s("%H:%M").should eq("01:00") + parse_time("11:00 AM", "%k:%M %P").to_s("%H:%M").should eq("11:00") + parse_time("12:00 AM", "%k:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("12:01 AM", "%k:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("13:00 AM", "%k:%M %P").to_s("%H:%M").should eq("13:00") + parse_time("23:00 AM", "%k:%M %P").to_s("%H:%M").should eq("23:00") + parse_time("24:00 AM", "%k:%M %P").to_s("%H:%M").should eq("00:00") + + parse_time("00:00 PM", "%H:%M %P").to_s("%H:%M").should eq("00:00") + parse_time("00:01 PM", "%H:%M %P").to_s("%H:%M").should eq("00:01") + parse_time("01:00 PM", "%H:%M %P").to_s("%H:%M").should eq("01:00") + parse_time("11:00 PM", "%H:%M %P").to_s("%H:%M").should eq("11:00") + parse_time("12:00 PM", "%H:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("12:01 PM", "%H:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("13:00 PM", "%H:%M %P").to_s("%H:%M").should eq("13:00") + parse_time("23:00 PM", "%H:%M %P").to_s("%H:%M").should eq("23:00") + parse_time("24:00 PM", "%H:%M %P").to_s("%H:%M").should eq("00:00") + + parse_time(" 0:00 PM", "%k:%M %P").to_s("%H:%M").should eq("00:00") + parse_time(" 0:01 PM", "%k:%M %P").to_s("%H:%M").should eq("00:01") + parse_time(" 1:00 PM", "%k:%M %P").to_s("%H:%M").should eq("01:00") + parse_time("11:00 PM", "%k:%M %P").to_s("%H:%M").should eq("11:00") + parse_time("12:00 PM", "%k:%M %P").to_s("%H:%M").should eq("12:00") + parse_time("12:01 PM", "%k:%M %P").to_s("%H:%M").should eq("12:01") + parse_time("13:00 PM", "%k:%M %P").to_s("%H:%M").should eq("13:00") + parse_time("23:00 PM", "%k:%M %P").to_s("%H:%M").should eq("23:00") + parse_time("24:00 PM", "%k:%M %P").to_s("%H:%M").should eq("00:00") + end + it "parses timezone" do patterns = {"%z", "%:z", "%::z"} diff --git a/spec/std/time/time_spec.cr b/spec/std/time/time_spec.cr index 57cbae04b884..99bba2bef7a0 100644 --- a/spec/std/time/time_spec.cr +++ b/spec/std/time/time_spec.cr @@ -173,6 +173,22 @@ describe Time do end end end + + it "accepts midnight 24:00" do + Time.utc(2020, 5, 21, 24, 0, 0).should eq Time.utc(2020, 5, 22, 0, 0, 0) + + expect_raises ArgumentError, "Invalid time" do + Time.utc(2020, 5, 21, 24, 0, 0, nanosecond: 1) + end + + expect_raises ArgumentError, "Invalid time" do + Time.utc(2020, 5, 21, 24, 0, 1) + end + + expect_raises ArgumentError, "Invalid time" do + Time.utc(2020, 5, 21, 24, 1, 0) + end + end end it "UNIX_EPOCH" do diff --git a/spec/std/va_list_spec.cr b/spec/std/va_list_spec.cr new file mode 100644 index 000000000000..79347c0638fe --- /dev/null +++ b/spec/std/va_list_spec.cr @@ -0,0 +1,40 @@ +require "./spec_helper" + +describe VaList do + it "works with C code" do + compile_and_run_source_with_c( + %( + #include + extern int foo_f(int,...); + int foo() { + return foo_f(3,1,2,3); + } + + int read_arg(va_list *ap) { + return va_arg(*ap, int); + } + ), + %( + lib LibFoo + fun foo : LibC::Int + fun read_arg(ap : LibC::VaList*) : LibC::Int + end + + fun foo_f(count : LibC::Int, ...) : LibC::Int + sum = 0 + VaList.open do |list| + ap = list.to_unsafe + count.times do |i| + sum += LibFoo.read_arg(pointerof(ap)) + end + end + sum + end + + puts LibFoo.foo + )) do |status, output| + status.success?.should be_true + output.to_i.should eq(6) + end + end +end diff --git a/spec/std/xml/builder_spec.cr b/spec/std/xml/builder_spec.cr index 24bc21ef802f..10ca184fe06e 100644 --- a/spec/std/xml/builder_spec.cr +++ b/spec/std/xml/builder_spec.cr @@ -170,19 +170,29 @@ describe XML::Builder do end end - it "writes cdata" do - assert_built(%{\n\n}) do |xml| - element("foo") do - cdata("hello") + describe "#cdata" do + it "writes cdata" do + assert_built(%{\n\n}) do |xml| + element("foo") do + cdata("hello") + end end end - end - it "writes cdata with block" do - assert_built(%{\n\n}) do |xml| - element("foo") do - cdata do - text "hello" + it "escapes ]]> sequences" do + assert_built(%{\nTwo]]]]>Three]]>\n}) do |xml| + element("foo") do + cdata("One]]>Two]]>Three") + end + end + end + + it "writes cdata with block" do + assert_built(%{\n\n}) do |xml| + element("foo") do + cdata do + text "hello" + end end end end diff --git a/spec/std/xml/xml_spec.cr b/spec/std/xml/xml_spec.cr index 2c6c6071b1f8..f1c3e684f8e4 100644 --- a/spec/std/xml/xml_spec.cr +++ b/spec/std/xml/xml_spec.cr @@ -242,6 +242,31 @@ describe XML do end end + it "escapes content" do + doc = XML.parse(<<-XML) + + John + XML + + root = doc.root.not_nil! + root.text = "" + root.text.should eq("") + + root.to_xml.should eq(%(<foo>)) + end + + it "escapes content HTML fragment" do + doc = XML.parse_html(<<-XML, XML::HTMLParserOptions.default | XML::HTMLParserOptions::NOIMPLIED | XML::HTMLParserOptions::NODEFDTD) +

foo

+ XML + + node = doc.children.first + node.text = "" + node.text.should eq("") + + node.to_xml.should eq(%(

<foo>

)) + end + it "gets empty content" do doc = XML.parse("") doc.children.first.content.should eq("") @@ -374,6 +399,12 @@ describe XML do res.should be_nil end + it "shows content when inspecting attribute" do + doc = XML.parse(%{}) + attr = doc.root.not_nil!.attributes.first + attr.inspect.should contain(%(content="baz")) + end + it ".build" do XML.build do |builder| builder.element "foo" { } diff --git a/spec/std/xml/xpath_spec.cr b/spec/std/xml/xpath_spec.cr index b58f5e803f05..9f243cf9f2ee 100644 --- a/spec/std/xml/xpath_spec.cr +++ b/spec/std/xml/xpath_spec.cr @@ -75,7 +75,7 @@ module XML doc.xpath_node("//invalid").should be_nil end - it "finds with namespace" do + it "finds with explicit namespace" do doc = XML.parse(%(\ @@ -89,6 +89,22 @@ module XML ns.prefix.should be_nil end + it "finds with implicit (root) namespaces" do + doc = XML.parse(%(\ + + + + + + )) + nodes = doc.xpath("//openSearch:feed/openSearch:something").as(NodeSet) + nodes.size.should eq(1) + nodes[0].name.should eq("something") + ns = nodes[0].namespace.not_nil! + ns.href.should eq("http://a9.com/-/spec/opensearchrss/1.0/") + ns.prefix.should eq("openSearch") + end + it "finds with root namespaces" do doc = XML.parse(%(\ @@ -103,6 +119,20 @@ module XML ns.prefix.should be_nil end + it "finds with root namespaces (using prefix)" do + doc = XML.parse(%(\ + + + + )) + nodes = doc.xpath("//openSearch:feed", namespaces: doc.root.not_nil!.namespaces).as(NodeSet) + nodes.size.should eq(1) + nodes[0].name.should eq("feed") + ns = nodes[0].namespace.not_nil! + ns.href.should eq("http://a9.com/-/spec/opensearchrss/1.0/") + ns.prefix.should eq("openSearch") + end + it "finds with variable binding" do doc = XML.parse(%(\ diff --git a/spec/std/yaml/mapping_spec.cr b/spec/std/yaml/mapping_spec.cr deleted file mode 100644 index 95920ce88e60..000000000000 --- a/spec/std/yaml/mapping_spec.cr +++ /dev/null @@ -1,561 +0,0 @@ -require "spec" -require "yaml" -require "../../support/finalize" - -private class YAMLPerson - YAML.mapping({ - name: String, - age: {type: Int32, nilable: true}, - }) - - def_equals name, age - - def initialize(@name : String) - end -end - -private class StrictYAMLPerson - YAML.mapping({ - name: {type: String}, - age: {type: Int32, nilable: true}, - }, true) -end - -private class YAMLWithBool - YAML.mapping value: Bool -end - -private class YAMLWithTime - YAML.mapping({ - value: {type: Time, converter: Time::Format.new("%F %T")}, - }) -end - -private class YAMLWithKey - YAML.mapping({ - key: String, - value: Int32, - pull: Int32, - }) -end - -private class YAMLWithPropertiesKey - YAML.mapping( - properties: Hash(String, String), - ) -end - -private class YAMLWithDefaults - YAML.mapping({ - a: {type: Int32, default: 11}, - b: {type: String, default: "Haha"}, - c: {type: Bool, default: true}, - d: {type: Bool, default: false}, - e: {type: Bool, nilable: true, default: false}, - f: {type: Int32, nilable: true, default: 1}, - g: {type: Int32, nilable: true, default: nil}, - h: {type: Array(Int32), default: [1, 2, 3]}, - i: String?, - }) -end - -private class YAMLWithAny - YAML.mapping({ - obj: YAML::Any, - }) - - def initialize(@obj) - end -end - -private class YAMLWithSmallIntegers - YAML.mapping({ - foo: Int16, - bar: Int8, - }) -end - -private class YAMLWithNilableTime - YAML.mapping({ - value: {type: Time, nilable: true, converter: Time::Format.new("%F")}, - }) - - def initialize - end -end - -private class YAMLWithTimeEpoch - YAML.mapping({ - value: {type: Time, converter: Time::EpochConverter}, - }) -end - -private class YAMLWithTimeEpochMillis - YAML.mapping({ - value: {type: Time, converter: Time::EpochMillisConverter}, - }) -end - -private class YAMLWithArrayConverter - YAML.mapping({ - values: {type: Array(Time), converter: YAML::ArrayConverter(Time::EpochConverter)}, - }) -end - -private class YAMLWithPresence - YAML.mapping({ - first_name: {type: String?, presence: true, nilable: true}, - last_name: {type: String?, presence: true, nilable: true}, - }) -end - -private class YAMLWithString - YAML.mapping({ - value: String, - }) -end - -class YAMLRecursive - YAML.mapping({ - name: String, - other: YAMLRecursive, - }) -end - -class YAMLRecursiveNilable - YAML.mapping({ - name: String, - other: YAMLRecursiveNilable?, - }) -end - -class YAMLRecursiveArray - YAML.mapping({ - name: String, - other: Array(YAMLRecursiveArray), - }) -end - -class YAMLRecursiveHash - YAML.mapping({ - name: String, - other: Hash(String, YAMLRecursiveHash), - }) -end - -private class YAMLWithQueryAttributes - YAML.mapping({ - foo?: Bool, - bar?: {type: Bool, default: false, presence: true, key: "is_bar"}, - }) -end - -private class YAMLWithOverwritingQueryAttributes - property foo : Symbol? - property bar : Symbol? - YAML.mapping({ - foo?: Bool, - bar?: {type: Bool, default: false, presence: true, key: "is_bar"}, - }) -end - -private class YAMLWithFinalize - YAML.mapping({ - value: YAML::Any, - }) - - property key : Symbol? - - def finalize - if key = self.key - State.inc(key) - end - end -end - -describe "YAML mapping" do - it "parses person" do - person = YAMLPerson.from_yaml("---\nname: John\nage: 30\n") - person.should be_a(YAMLPerson) - person.name.should eq("John") - person.age.should eq(30) - end - - it "parses person without age" do - person = YAMLPerson.from_yaml("---\nname: John\n") - person.should be_a(YAMLPerson) - person.name.should eq("John") - person.name.size.should eq(4) # This verifies that name is not nilable - person.age.should be_nil - end - - it "parses person with blank age" do - person = YAMLPerson.from_yaml("---\nname: John\nage:\n") - person.should be_a(YAMLPerson) - person.name.should eq("John") - person.name.size.should eq(4) # This verifies that name is not nilable - person.age.should be_nil - end - - it "parses array of people" do - people = Array(YAMLPerson).from_yaml("---\n- name: John\n- name: Doe\n") - people.size.should eq(2) - people[0].name.should eq("John") - people[1].name.should eq("Doe") - end - - it "parses array of people with merge" do - yaml = <<-YAML - - &1 - name: foo - age: 1 - - - <<: *1 - age: 2 - YAML - - people = Array(YAMLPerson).from_yaml(yaml) - people[1].name.should eq("foo") - people[1].age.should eq(2) - end - - it "parses array of people with merge, doesn't hang on infinite recursion" do - yaml = <<-YAML - - &1 - name: foo - <<: *1 - <<: [ *1, *1 ] - age: 1 - YAML - - people = Array(YAMLPerson).from_yaml(yaml) - people[0].name.should eq("foo") - people[0].age.should eq(1) - end - - it "parses person with unknown attributes" do - person = YAMLPerson.from_yaml("---\nname: John\nunknown: [1, 2, 3]\nage: 30\n") - person.should be_a(YAMLPerson) - person.name.should eq("John") - person.age.should eq(30) - end - - it "parses strict person with unknown attributes" do - ex = expect_raises YAML::ParseException, "Unknown yaml attribute: foo" do - StrictYAMLPerson.from_yaml <<-YAML - --- - name: John - foo: [1, 2, 3] - age: 30 - YAML - end - ex.location.should eq({3, 1}) - end - - it "does to_yaml" do - person = YAMLPerson.from_yaml("---\nname: John\nage: 30\n") - person2 = YAMLPerson.from_yaml(person.to_yaml) - person2.should eq(person) - end - - it "doesn't emit null when doing to_yaml" do - person = YAMLPerson.from_yaml("---\nname: John\n") - (person.to_yaml =~ /age/).should be_falsey - end - - it "raises if non-nilable attribute is nil" do - ex = expect_raises YAML::ParseException, "Missing yaml attribute: name" do - YAMLPerson.from_yaml <<-YAML - --- - age: 30 - YAML - end - ex.location.should eq({2, 1}) - end - - it "doesn't raises on false value when not-nil" do - yaml = YAMLWithBool.from_yaml("---\nvalue: false\n") - yaml.value.should be_false - end - - it "parses yaml with Time::Format converter" do - yaml = YAMLWithTime.from_yaml("---\nvalue: 2014-10-31 23:37:16\n") - yaml.value.should be_a(Time) - yaml.value.to_s.should eq("2014-10-31 23:37:16 UTC") - yaml.value.should eq(Time.utc(2014, 10, 31, 23, 37, 16)) - yaml.to_yaml.should eq("---\nvalue: 2014-10-31 23:37:16\n") - end - - it "parses YAML with mapping key named 'key'" do - yaml = YAMLWithKey.from_yaml("---\nkey: foo\nvalue: 1\npull: 2") - yaml.key.should eq("foo") - yaml.value.should eq(1) - yaml.pull.should eq(2) - end - - it "outputs YAML with properties key" do - input = { - properties: {"foo" => "bar"}, - }.to_yaml - yaml = YAMLWithPropertiesKey.from_yaml(input) - yaml.to_yaml.should eq(input) - end - - it "allows small types of integer" do - yaml = YAMLWithSmallIntegers.from_yaml(%({"foo": 21, "bar": 7})) - - yaml.foo.should eq(21) - typeof(yaml.foo).should eq(Int16) - - yaml.bar.should eq(7) - typeof(yaml.bar).should eq(Int8) - end - - it "parses recursive" do - yaml = <<-YAML - --- &1 - name: foo - other: *1 - YAML - - rec = YAMLRecursive.from_yaml(yaml) - rec.name.should eq("foo") - rec.other.should be(rec) - end - - it "parses recursive nilable (1)" do - yaml = <<-YAML - --- &1 - name: foo - other: *1 - YAML - - rec = YAMLRecursiveNilable.from_yaml(yaml) - rec.name.should eq("foo") - rec.other.should be(rec) - end - - it "parses recursive nilable (2)" do - yaml = <<-YAML - --- &1 - name: foo - YAML - - rec = YAMLRecursiveNilable.from_yaml(yaml) - rec.name.should eq("foo") - rec.other.should be_nil - end - - it "parses recursive array" do - yaml = <<-YAML - --- - name: foo - other: &1 - - name: bar - other: *1 - YAML - - rec = YAMLRecursiveArray.from_yaml(yaml) - rec.other[0].other.should be(rec.other) - end - - it "parses recursive hash" do - yaml = <<-YAML - --- - name: foo - other: &1 - foo: - name: bar - other: *1 - YAML - - rec = YAMLRecursiveHash.from_yaml(yaml) - rec.other["foo"].other.should be(rec.other) - end - - describe "parses YAML with defaults" do - it "mixed" do - json = YAMLWithDefaults.from_yaml(%({"a":1,"b":"bla"})) - json.a.should eq 1 - json.b.should eq "bla" - - json = YAMLWithDefaults.from_yaml(%({"a":1})) - json.a.should eq 1 - json.b.should eq "Haha" - - json = YAMLWithDefaults.from_yaml(%({"b":"bla"})) - json.a.should eq 11 - json.b.should eq "bla" - - json = YAMLWithDefaults.from_yaml(%({})) - json.a.should eq 11 - json.b.should eq "Haha" - end - - it "mixes with all defaults (#2873)" do - yaml = YAMLWithDefaults.from_yaml("") - yaml.a.should eq 11 - yaml.b.should eq "Haha" - end - - it "raises when not a mapping or empty scalar" do - expect_raises(YAML::ParseException) do - YAMLWithDefaults.from_yaml("1") - end - - expect_raises(YAML::ParseException) do - YAMLWithDefaults.from_yaml("[1]") - end - end - - it "bool" do - json = YAMLWithDefaults.from_yaml(%({})) - json.c.should eq true - typeof(json.c).should eq Bool - json.d.should eq false - typeof(json.d).should eq Bool - - json = YAMLWithDefaults.from_yaml(%({"c":false})) - json.c.should eq false - json = YAMLWithDefaults.from_yaml(%({"c":true})) - json.c.should eq true - - json = YAMLWithDefaults.from_yaml(%({"d":false})) - json.d.should eq false - json = YAMLWithDefaults.from_yaml(%({"d":true})) - json.d.should eq true - end - - it "with nilable" do - json = YAMLWithDefaults.from_yaml(%({})) - - json.e.should eq false - typeof(json.e).should eq(Bool | Nil) - - json.f.should eq 1 - typeof(json.f).should eq(Int32 | Nil) - - json.g.should eq nil - typeof(json.g).should eq(Int32 | Nil) - - json = YAMLWithDefaults.from_yaml(%({"e":false})) - json.e.should eq false - json = YAMLWithDefaults.from_yaml(%({"e":true})) - json.e.should eq true - - json = YAMLWithDefaults.from_yaml(%({})) - json.i.should be_nil - - json = YAMLWithDefaults.from_yaml(%({"i":"bla"})) - json.i.should eq("bla") - end - - it "create new array every time" do - json = YAMLWithDefaults.from_yaml(%({})) - json.h.should eq [1, 2, 3] - json.h << 4 - json.h.should eq [1, 2, 3, 4] - - json = YAMLWithDefaults.from_yaml(%({})) - json.h.should eq [1, 2, 3] - end - end - - it "parses YAML with any" do - yaml = YAMLWithAny.from_yaml("obj: hello") - yaml.obj.as_s.should eq("hello") - - yaml = YAMLWithAny.from_yaml({:obj => %w(foo bar)}.to_yaml) - yaml.obj[1].as_s.should eq("bar") - - yaml = YAMLWithAny.from_yaml({:obj => {:foo => :bar}}.to_yaml) - yaml.obj["foo"].as_s.should eq("bar") - end - - it "outputs with converter when nilable" do - yaml = YAMLWithNilableTime.new - yaml.to_yaml.should eq("--- {}\n") - end - - it "uses Time::EpochConverter" do - string = %({"value":1459859781}) - yaml = YAMLWithTimeEpoch.from_yaml(string) - yaml.value.should be_a(Time) - yaml.value.should eq(Time.unix(1459859781)) - yaml.to_yaml.should eq("---\nvalue: 1459859781\n") - end - - it "uses Time::EpochMillisConverter" do - string = %({"value":1459860483856}) - yaml = YAMLWithTimeEpochMillis.from_yaml(string) - yaml.value.should be_a(Time) - yaml.value.should eq(Time.unix_ms(1459860483856)) - yaml.to_yaml.should eq("---\nvalue: 1459860483856\n") - end - - it "uses YAML::ArrayConverter" do - string = %({"values":[1459859781,1567628762]}) - yaml = YAMLWithArrayConverter.from_yaml(string) - yaml.values.should be_a(Array(Time)) - yaml.values.should eq([Time.unix(1459859781), Time.unix(1567628762)]) - yaml.to_yaml.should eq(%(---\nvalues:\n- 1459859781\n- 1567628762\n)) - end - - describe "parses YAML with presence markers" do - it "parses person with absent attributes" do - yaml = YAMLWithPresence.from_yaml("---\nfirst_name:\n") - yaml.first_name.should be_nil - yaml.first_name_present?.should be_true - yaml.last_name.should be_nil - yaml.last_name_present?.should be_false - end - end - - describe "with query attributes" do - it "defines query getter" do - yaml = YAMLWithQueryAttributes.from_yaml(%({"foo": true})) - yaml.foo?.should be_true - yaml.bar?.should be_false - end - - it "defines non-query setter and presence methods" do - yaml = YAMLWithQueryAttributes.from_yaml(%({"foo": false})) - yaml.bar_present?.should be_false - yaml.bar = true - yaml.bar?.should be_true - end - - it "maps non-query attributes" do - yaml = YAMLWithQueryAttributes.from_yaml(%({"foo": false, "is_bar": false})) - yaml.bar_present?.should be_true - yaml.bar?.should be_false - yaml.bar = true - yaml.to_yaml.should eq(%(---\nfoo: false\nis_bar: true\n)) - end - - it "raises if non-nilable attribute is nil" do - ex = expect_raises YAML::ParseException, "Missing yaml attribute: foo" do - YAMLWithQueryAttributes.from_yaml(%({"is_bar": true})) - end - ex.location.should eq({1, 1}) - end - - it "overwrites non-query attributes" do - yaml = YAMLWithOverwritingQueryAttributes.from_yaml(%({"foo": true})) - typeof(yaml.@foo).should eq(Bool) - typeof(yaml.@bar).should eq(Bool) - end - end - - it "calls #finalize" do - assert_finalizes(:yaml) { YAMLWithFinalize.from_yaml("---\nvalue: 1\n") } - end - - it "parses string even if it looks like a number" do - yaml = YAMLWithString.from_yaml <<-YAML - --- - value: 12.34 - YAML - yaml.value.should eq("12.34") - end -end diff --git a/spec/std/yaml/serializable_spec.cr b/spec/std/yaml/serializable_spec.cr index 74001ea667ba..701c2a939ccf 100644 --- a/spec/std/yaml/serializable_spec.cr +++ b/spec/std/yaml/serializable_spec.cr @@ -497,7 +497,7 @@ describe "YAML::Serializable" do it "emit_nulls option" do person = YAMLAttrPersonEmittingNullsByOptions.from_yaml("---\nname: John\n") - person.to_yaml.should eq "---\nname: John\nage: \nvalue1: \n" + person.to_yaml.should match /\A---\nname: John\nage: ?\nvalue1: ?\n\z/ end it "parses yaml with Time::Format converter" do @@ -528,7 +528,7 @@ describe "YAML::Serializable" do it "outputs with converter when nilable when emit_null is true" do yaml = YAMLAttrWithNilableTimeEmittingNull.new - yaml.to_yaml.should eq("---\nvalue: \n") + yaml.to_yaml.should match(/\A---\nvalue: ?\n\z/) end it "outputs YAML with properties key" do diff --git a/spec/std/yaml/serialization_spec.cr b/spec/std/yaml/serialization_spec.cr index 6a09eb59cd9c..8f3b0ed42805 100644 --- a/spec/std/yaml/serialization_spec.cr +++ b/spec/std/yaml/serialization_spec.cr @@ -1,3 +1,4 @@ +require "../spec_helper" require "spec" require "yaml" {% unless flag?(:win32) %} @@ -71,13 +72,19 @@ describe "YAML serialization" do it "does Float32#from_yaml" do Float32.from_yaml("1.5").should eq(1.5_f32) + Float32.from_yaml(".nan").nan?.should be_true Float32.from_yaml(".inf").should eq(Float32::INFINITY) + Float32.from_yaml("-.inf").should eq(-Float32::INFINITY) end it "does Float64#from_yaml" do value = Float64.from_yaml("1.5") value.should eq(1.5) value.should be_a(Float64) + + Float64.from_yaml(".nan").nan?.should be_true + Float64.from_yaml(".inf").should eq(Float64::INFINITY) + Float64.from_yaml("-.inf").should eq(-Float64::INFINITY) end it "does Array#from_yaml" do @@ -275,10 +282,38 @@ describe "YAML serialization" do Int32.from_yaml(1.to_yaml).should eq(1) end + it "does for Float32" do + Float32.from_yaml(1.5_f32.to_yaml).should eq(1.5_f32) + end + + it "does for Float32 (infinity)" do + Float32.from_yaml(Float32::INFINITY.to_yaml).should eq(Float32::INFINITY) + end + + it "does for Float32 (-infinity)" do + Float32.from_yaml((-Float32::INFINITY).to_yaml).should eq(-Float32::INFINITY) + end + + it "does for Float32 (nan)" do + Float32.from_yaml(Float32::NAN.to_yaml).nan?.should be_true + end + it "does for Float64" do Float64.from_yaml(1.5.to_yaml).should eq(1.5) end + it "does for Float64 (infinity)" do + Float64.from_yaml(Float64::INFINITY.to_yaml).should eq(Float64::INFINITY) + end + + it "does for Float64 (-infinity)" do + Float64.from_yaml((-Float64::INFINITY).to_yaml).should eq(-Float64::INFINITY) + end + + it "does for Float64 (nan)" do + Float64.from_yaml(Float64::NAN.to_yaml).nan?.should be_true + end + it "does for String" do String.from_yaml("hello".to_yaml).should eq("hello") end @@ -349,7 +384,7 @@ describe "YAML serialization" do it "does for utc time" do time = Time.utc(2010, 11, 12, 1, 2, 3) - assert_yaml_document_end(time.to_yaml, "--- 2010-11-12 01:02:03\n") + assert_yaml_document_end(time.to_yaml, "--- 2010-11-12 01:02:03.000000000\n") end it "does for time at date" do @@ -385,9 +420,9 @@ describe "YAML serialization" do :null => nil, } - expected = "---\nhello: World\ninteger: 2\nfloat: 3.5\nhash:\n a: 1\n b: 2\narray:\n- 1\n- 2\n- 3\nnull: \n" + expected = /\A---\nhello: World\ninteger: 2\nfloat: 3.5\nhash:\n a: 1\n b: 2\narray:\n- 1\n- 2\n- 3\nnull: ?\n\z/ - data.to_yaml.should eq(expected) + data.to_yaml.should match(expected) end it "writes to a stream" do @@ -408,7 +443,7 @@ describe "YAML serialization" do h[1] = 2 h[h] = h - h.to_yaml.should eq("--- &1\n1: 2\n*1: *1\n") + h.to_yaml.should match(/\A--- &1\n1: 2\n\*1 ?: \*1\n\z/) end end end diff --git a/spec/std/zip/zip_file_spec.cr b/spec/std/zip/zip_file_spec.cr deleted file mode 100644 index 7558d101479a..000000000000 --- a/spec/std/zip/zip_file_spec.cr +++ /dev/null @@ -1,132 +0,0 @@ -require "../spec_helper" -require "zip" - -describe Zip do - it "reads file from memory" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.add "foo.txt", "contents of foo" - zip.add "bar.txt", "contents of bar" - end - - io.rewind - - Zip::File.open(io) do |zip| - entries = zip.entries - entries.size.should eq(2) - - foo = entries[0] - foo.filename.should eq("foo.txt") - - bar = entries[1] - bar.filename.should eq("bar.txt") - - zip["foo.txt"].filename.should eq("foo.txt") - zip["bar.txt"].filename.should eq("bar.txt") - zip["baz.txt"]?.should be_nil - - foo.open do |foo_io| - bar.open do |bar_io| - foo_io.gets_to_end.should eq("contents of foo") - bar_io.gets_to_end.should eq("contents of bar") - end - end - end - end - - it "reads file from file system" do - filename = datapath("file.zip") - - begin - File.open(filename, "w") do |file| - Zip::Writer.open(file) do |zip| - zip.add "foo.txt", "contents of foo" - zip.add "bar.txt", "contents of bar" - end - end - - File.open(filename, "r") do |file| - Zip::File.open(file) do |zip| - entries = zip.entries - entries.size.should eq(2) - - foo = entries[0] - foo.filename.should eq("foo.txt") - - bar = entries[1] - bar.filename.should eq("bar.txt") - - zip["foo.txt"].filename.should eq("foo.txt") - zip["bar.txt"].filename.should eq("bar.txt") - zip["baz.txt"]?.should be_nil - - foo.open do |foo_io| - bar.open do |bar_io| - foo_io.gets_to_end.should eq("contents of foo") - bar_io.gets_to_end.should eq("contents of bar") - end - end - end - end - ensure - File.delete(filename) - end - end - - it "writes comment" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.add Zip::Writer::Entry.new("foo.txt", comment: "some comment"), - "contents of foo" - end - - io.rewind - - Zip::File.open(io) do |zip| - zip["foo.txt"].comment.should eq("some comment") - end - end - - it "reads big file" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - 100.times do |i| - zip.add "foo#{i}.txt", "some contents #{i}" - end - end - - io.rewind - - Zip::File.open(io) do |zip| - zip.entries.size.should eq(100) - end - end - - it "reads zip file with different extra in local file header and central directory header" do - Zip::File.open(datapath("test.zip")) do |zip| - zip.entries.size.should eq(2) - zip["one.txt"].open(&.gets_to_end).should eq("One") - zip["two.txt"].open(&.gets_to_end).should eq("Two") - end - end - - it "reads zip comment" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.comment = "zip comment" - end - - io.rewind - - Zip::File.open(io) do |zip| - zip.comment.should eq("zip comment") - end - end - - typeof(Zip::File.new("file.zip")) - typeof(Zip::File.open("file.zip") { }) -end diff --git a/spec/std/zip/zip_spec.cr b/spec/std/zip/zip_spec.cr deleted file mode 100644 index fdb88c7ad5fa..000000000000 --- a/spec/std/zip/zip_spec.cr +++ /dev/null @@ -1,219 +0,0 @@ -require "../spec_helper" -require "zip" - -describe Zip do - it "writes and reads to memory" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.add "foo.txt", &.print("contents of foo") - zip.add "bar.txt", &.print("contents of bar") - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.file?.should be_true - entry.dir?.should be_false - entry.filename.should eq("foo.txt") - entry.compression_method.should eq(Zip::CompressionMethod::DEFLATED) - entry.crc32.should eq(0) - entry.compressed_size.should eq(0) - entry.uncompressed_size.should eq(0) - entry.extra.empty?.should be_true - entry.io.gets_to_end.should eq("contents of foo") - - entry = zip.next_entry.not_nil! - entry.filename.should eq("bar.txt") - entry.io.gets_to_end.should eq("contents of bar") - - zip.next_entry.should be_nil - end - end - - it "writes entry" do - io = IO::Memory.new - - time = Time.utc(2017, 1, 14, 2, 3, 4) - extra = Bytes[1, 2, 3, 4] - - Zip::Writer.open(io) do |zip| - zip.add(Zip::Writer::Entry.new("foo.txt", time: time, extra: extra)) do |io| - io.print("contents of foo") - end - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("foo.txt") - entry.time.should eq(time) - entry.extra.should eq(extra) - entry.io.gets_to_end.should eq("contents of foo") - end - end - - it "writes entry uncompressed" do - io = IO::Memory.new - - text = "contents of foo" - crc32 = Digest::CRC32.checksum(text) - - Zip::Writer.open(io) do |zip| - entry = Zip::Writer::Entry.new("foo.txt") - entry.compression_method = Zip::CompressionMethod::STORED - entry.crc32 = crc32 - entry.compressed_size = text.bytesize.to_u32 - entry.uncompressed_size = text.bytesize.to_u32 - zip.add entry, &.print(text) - - entry = Zip::Writer::Entry.new("bar.txt") - entry.compression_method = Zip::CompressionMethod::STORED - entry.crc32 = crc32 - entry.compressed_size = text.bytesize.to_u32 - entry.uncompressed_size = text.bytesize.to_u32 - zip.add entry, &.print(text) - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("foo.txt") - entry.compression_method.should eq(Zip::CompressionMethod::STORED) - entry.crc32.should eq(crc32) - entry.compressed_size.should eq(text.bytesize) - entry.uncompressed_size.should eq(text.bytesize) - entry.io.gets_to_end.should eq(text) - - entry = zip.next_entry.not_nil! - entry.filename.should eq("bar.txt") - entry.io.gets_to_end.should eq(text) - end - end - - it "writes entry uncompressed and reads with Zip::File" do - io = IO::Memory.new - - text = "contents of foo" - crc32 = Digest::CRC32.checksum(text) - - Zip::Writer.open(io) do |zip| - entry = Zip::Writer::Entry.new("foo.txt") - entry.compression_method = Zip::CompressionMethod::STORED - entry.crc32 = crc32 - entry.compressed_size = text.bytesize.to_u32 - entry.uncompressed_size = text.bytesize.to_u32 - zip.add entry, &.print(text) - end - - io.rewind - - Zip::File.open(io) do |zip| - zip.entries.size.should eq(1) - entry = zip.entries.first - entry.filename.should eq("foo.txt") - entry.open(&.gets_to_end).should eq(text) - end - end - - it "adds a directory" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.add_dir "one" - zip.add_dir "two/" - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("one/") - entry.file?.should be_false - entry.dir?.should be_true - entry.io.gets_to_end.should eq("") - - entry = zip.next_entry.not_nil! - entry.filename.should eq("two/") - entry.dir?.should be_true - entry.io.gets_to_end.should eq("") - end - end - - it "writes string" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.add "foo.txt", "contents of foo" - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("foo.txt") - entry.io.gets_to_end.should eq("contents of foo") - end - end - - it "writes bytes" do - io = IO::Memory.new - - Zip::Writer.open(io) do |zip| - zip.add "foo.txt", "contents of foo".to_slice - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("foo.txt") - entry.io.gets_to_end.should eq("contents of foo") - end - end - - it "writes io" do - io = IO::Memory.new - data = IO::Memory.new("contents of foo") - - Zip::Writer.open(io) do |zip| - zip.add "foo.txt", data - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("foo.txt") - entry.io.gets_to_end.should eq("contents of foo") - end - end - - it "writes file" do - io = IO::Memory.new - filename = datapath("test_file.txt") - - Zip::Writer.open(io) do |zip| - file = File.open(filename) - zip.add "foo.txt", file - file.closed?.should be_true - end - - io.rewind - - Zip::Reader.open(io) do |zip| - entry = zip.next_entry.not_nil! - entry.filename.should eq("foo.txt") - entry.io.gets_to_end.should eq(File.read(filename)) - end - end - - typeof(Zip::Reader.new("file.zip")) - typeof(Zip::Reader.open("file.zip") { }) - - typeof(Zip::Writer.new("file.zip")) - typeof(Zip::Writer.open("file.zip") { }) -end diff --git a/spec/std/zlib/reader_spec.cr b/spec/std/zlib/reader_spec.cr deleted file mode 100644 index 23a1953ff169..000000000000 --- a/spec/std/zlib/reader_spec.cr +++ /dev/null @@ -1,88 +0,0 @@ -require "spec" -require "zlib" - -private def new_sample_io - io = IO::Memory.new - "789c2bc9c82c5600a2448592d4e21285e292a2ccbc74054520e00200854f087b".scan(/../).each do |match| - io.write_byte match[0].to_u8(16) - end - io.rewind -end - -module Zlib - describe Reader do - it "should be able to read" do - io = new_sample_io - - reader = Reader.new(io) - - str = String::Builder.build do |builder| - IO.copy(reader, builder) - end - - str.should eq("this is a test string !!!!\n") - reader.read(Bytes.new(10)).should eq(0) - end - - it "rewinds" do - io = new_sample_io - - reader = Reader.new(io) - reader.gets(3).should eq("thi") - reader.rewind - reader.gets_to_end.should eq("this is a test string !!!!\n") - end - - it "can be closed without sync" do - io = IO::Memory.new(Bytes[120, 156, 3, 0, 0, 0, 0, 1]) - reader = Reader.new(io) - reader.close - reader.closed?.should be_true - io.closed?.should be_false - - expect_raises IO::Error, "Closed stream" do - reader.gets - end - end - - it "can be closed with sync (1)" do - io = IO::Memory.new(Bytes[120, 156, 3, 0, 0, 0, 0, 1]) - reader = Reader.new(io, sync_close: true) - reader.close - reader.closed?.should be_true - io.closed?.should be_true - end - - it "can be closed with sync (2)" do - io = IO::Memory.new(Bytes[120, 156, 3, 0, 0, 0, 0, 1]) - reader = Reader.new(io) - reader.sync_close = true - reader.close - reader.closed?.should be_true - io.closed?.should be_true - end - - it "should not read from empty stream" do - io = IO::Memory.new(Bytes[120, 156, 3, 0, 0, 0, 0, 1]) - reader = Reader.new(io) - reader.read_byte.should be_nil - end - - it "should not freeze when reading empty slice" do - io = new_sample_io - reader = Reader.new(io) - slice = Bytes.empty - reader.read(slice).should eq(0) - end - - it "should raise buffer error on error (#6575)" do - io = IO::Memory.new("x\x9C4\xC9\xD1\n@@\u0010\u0005\xD0\u007F\xB9ϻeEj~E\xD2`B\xAD\xA55H\x9B\u007F\xE7\xC5۩\x93\xA0\xA0pxo\xB0\xFFX7\x90\xCB\f\u0006P\xC2$\u001C\xB5\u0013\xD6v\u000E*\xF1d\u000F*\\^~\xDFj\xE4^@5FV\xB9\xF8\xB6[\u001C\xEC\xC2s\xB0\x99\xD3\n\xCD\xF3\xBC\u0000\u0000\u0000\xFF\xFF") - - reader = Reader.new(io) - - expect_raises(Compress::Deflate::Error, "deflate: buffer error") do - reader.gets_to_end - end - end - end -end diff --git a/spec/std/zlib/stress_spec.cr b/spec/std/zlib/stress_spec.cr deleted file mode 100644 index dd0005c8b55e..000000000000 --- a/spec/std/zlib/stress_spec.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "spec" -require "zlib" - -module Zlib - describe Zlib do - it "write read should be inverse with random string" do - expected = String.build do |io| - 1_000_000.times { rand(2000).to_i.to_s(32, io) } - end - - io = IO::Memory.new - - writer = Writer.new(io) - writer.print expected - writer.close - - io.rewind - reader = Reader.new(io) - reader.gets_to_end.should eq(expected) - end - - it "write read should be inverse (utf-8)" do - expected = "日本さん語日本さん語" - - io = IO::Memory.new - - writer = Writer.new(io) - writer.print expected - writer.close - - io.rewind - reader = Reader.new(io) - reader.gets_to_end.should eq(expected) - end - end -end diff --git a/spec/std/zlib/writer_spec.cr b/spec/std/zlib/writer_spec.cr deleted file mode 100644 index 3966a3425678..000000000000 --- a/spec/std/zlib/writer_spec.cr +++ /dev/null @@ -1,71 +0,0 @@ -require "spec" -require "zlib" - -module Zlib - describe Writer do - it "should be able to write" do - message = "this is a test string !!!!\n" - io = IO::Memory.new - - writer = Writer.new(io) - - io.bytesize.should eq(0) - writer.flush - io.bytesize.should_not eq(0) - - writer.print message - writer.close - - io.rewind - reader = Reader.new(io) - reader.gets_to_end.should eq(message) - end - - it "can be closed without sync" do - io = IO::Memory.new - writer = Writer.new(io) - writer.close - writer.closed?.should be_true - io.closed?.should be_false - - expect_raises IO::Error, "Closed stream" do - writer.print "a" - end - end - - it "can be closed with sync (1)" do - io = IO::Memory.new - writer = Writer.new(io, sync_close: true) - writer.close - writer.closed?.should be_true - io.closed?.should be_true - end - - it "can be closed with sync (2)" do - io = IO::Memory.new - writer = Writer.new(io) - writer.sync_close = true - writer.close - writer.closed?.should be_true - io.closed?.should be_true - end - - it "can be flushed" do - io = IO::Memory.new - writer = Writer.new(io) - - writer.print "this" - io.to_slice.hexstring.should eq("789c") - - writer.flush - (io.to_slice.hexstring.size > 4).should be_true - - writer.print " is a test string !!!!\n" - writer.close - - io.rewind - reader = Reader.new(io) - reader.gets_to_end.should eq("this is a test string !!!!\n") - end - end -end diff --git a/spec/support/env.cr b/spec/support/env.cr new file mode 100644 index 000000000000..9e99f903c126 --- /dev/null +++ b/spec/support/env.cr @@ -0,0 +1,16 @@ +def with_env(**values) + old_values = {} of String => String? + begin + values.each do |key, value| + key = key.to_s + old_values[key] = ENV[key]? + ENV[key] = value + end + + yield + ensure + old_values.each do |key, old_value| + ENV[key] = old_value + end + end +end diff --git a/spec/support/fibers.cr b/spec/support/fibers.cr index 4856233c338b..50e5b38e300f 100644 --- a/spec/support/fibers.cr +++ b/spec/support/fibers.cr @@ -1,11 +1,16 @@ -def wait_until_blocked(f : Fiber) +def wait_until_blocked(f : Fiber, timeout = 5.seconds) + now = Time.monotonic + until f.resumable? Fiber.yield + raise "fiber failed to block within #{timeout}" if (Time.monotonic - now) > timeout end end -def wait_until_finished(f : Fiber) +def wait_until_finished(f : Fiber, timeout = 5.seconds) + now = Time.monotonic until f.dead? Fiber.yield + raise "fiber failed to finish within #{timeout}" if (Time.monotonic - now) > timeout end end diff --git a/spec/support/retry.cr b/spec/support/retry.cr new file mode 100644 index 000000000000..1c368dee0134 --- /dev/null +++ b/spec/support/retry.cr @@ -0,0 +1,17 @@ +def retry(n = 5) + exception = nil + n.times do |i| + yield + rescue ex + exception = ex + if i == 0 + Fiber.yield + else + sleep 0.01 * (2**i) + end + else + return + end + + raise exception.not_nil! +end diff --git a/spec/support/syntax.cr b/spec/support/syntax.cr index 8d036541256a..67634180f565 100644 --- a/spec/support/syntax.cr +++ b/spec/support/syntax.cr @@ -154,8 +154,11 @@ def assert_syntax_error(str, message = nil, line = nil, column = nil, metafile = end end -def parse(string, wants_doc = false) +def parse(string, wants_doc = false, filename = nil) parser = Parser.new(string) parser.wants_doc = wants_doc + if filename + parser.filename = filename + end parser.parse end diff --git a/spec/support/tempfile.cr b/spec/support/tempfile.cr index 4f527e35329c..036c161fcae4 100644 --- a/spec/support/tempfile.cr +++ b/spec/support/tempfile.cr @@ -28,14 +28,56 @@ def with_tempfile(*paths, file = __FILE__) ensure if SPEC_TEMPFILE_CLEANUP paths.each do |path| - FileUtils.rm_r(path) if File.exists?(path) + rm_rf(path) if File.exists?(path) end end end end +def with_temp_executable(name, file = __FILE__) + {% if flag?(:win32) %} + name += ".exe" + {% end %} + with_tempfile(name, file: file) do |tempname| + yield tempname + end +end + +def with_temp_c_object_file(c_code, file = __FILE__) + obj_ext = {{ flag?(:win32) ? ".obj" : ".o" }} + with_tempfile("temp_c.c", "temp_c#{obj_ext}", file: file) do |c_filename, o_filename| + File.write(c_filename, c_code) + + {% if flag?(:win32) %} + `cl.exe /nologo /c #{Process.quote(c_filename)} #{Process.quote("/Fo#{o_filename}")}`.should be_truthy + {% else %} + `#{ENV["CC"]? || "cc"} #{Process.quote(c_filename)} -c -o #{Process.quote(o_filename)}`.should be_truthy + {% end %} + + yield o_filename + end +end + if SPEC_TEMPFILE_CLEANUP at_exit do - FileUtils.rm_r(SPEC_TEMPFILE_PATH) if Dir.exists?(SPEC_TEMPFILE_PATH) + rm_rf(SPEC_TEMPFILE_PATH) if Dir.exists?(SPEC_TEMPFILE_PATH) + end +end + +private def rm_rf(path : String) : Nil + if Dir.exists?(path) && !File.symlink?(path) + Dir.each_child(path) do |entry| + src = File.join(path, entry) + rm_rf(src) + end + Dir.delete(path) + else + begin + File.delete(path) + rescue File::AccessDeniedError + # To be able to delete read-only files (e.g. ones under .git/) on Windows. + File.chmod(path, 0o666) + File.delete(path) + end end end diff --git a/spec/support/win32.cr b/spec/support/win32.cr new file mode 100644 index 000000000000..da82d1ca24c3 --- /dev/null +++ b/spec/support/win32.cr @@ -0,0 +1,19 @@ +require "spec" + +{% if flag?(:win32) %} + def pending_win32(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) + pending("#{description} [win32]", file, line, end_line) + end + + def pending_win32(*, describe, file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) + pending_win32(describe, file, line, end_line) { } + end +{% else %} + def pending_win32(description = "assert", file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) + it(description, file, line, end_line, &block) + end + + def pending_win32(*, describe, file = __FILE__, line = __LINE__, end_line = __END_LINE__, &block) + describe(describe, file, line, end_line, &block) + end +{% end %} diff --git a/spec/win32_std_spec.cr b/spec/win32_std_spec.cr index f82d89155c29..935236a300af 100644 --- a/spec/win32_std_spec.cr +++ b/spec/win32_std_spec.cr @@ -1,7 +1,6 @@ # This file is autogenerated by `scripts/generate_windows_spec.sh` # 2020-04-20 22:36:41+02:00 -require "./std/adler32_spec.cr" require "./std/array_spec.cr" require "./std/atomic_spec.cr" require "./std/base64_spec.cr" @@ -30,7 +29,6 @@ require "./std/compress/zlib/stress_spec.cr" require "./std/compress/zlib/writer_spec.cr" # require "./std/concurrent/select_spec.cr" (failed to run) require "./std/concurrent_spec.cr" -require "./std/crc32_spec.cr" require "./std/crypto/bcrypt/base64_spec.cr" require "./std/crypto/bcrypt/password_spec.cr" require "./std/crypto/bcrypt_spec.cr" @@ -55,19 +53,17 @@ require "./std/ecr/ecr_spec.cr" require "./std/enum_spec.cr" require "./std/enumerable_spec.cr" require "./std/env_spec.cr" -# require "./std/exception/call_stack_spec.cr" +require "./std/exception/call_stack_spec.cr" require "./std/exception_spec.cr" require "./std/file/tempfile_spec.cr" require "./std/file_spec.cr" # require "./std/file_utils_spec.cr" -require "./std/flate/flate_spec.cr" require "./std/float_printer/diy_fp_spec.cr" require "./std/float_printer/grisu3_spec.cr" require "./std/float_printer/ieee_spec.cr" require "./std/float_printer_spec.cr" require "./std/float_spec.cr" require "./std/gc_spec.cr" -require "./std/gzip/gzip_spec.cr" require "./std/hash_spec.cr" require "./std/html_spec.cr" require "./std/http/chunked_content_spec.cr" @@ -100,7 +96,7 @@ require "./std/io/argf_spec.cr" require "./std/io/buffered_spec.cr" require "./std/io/byte_format_spec.cr" require "./std/io/delimited_spec.cr" -# require "./std/io/file_descriptor_spec.cr" (failed codegen) +require "./std/io/file_descriptor_spec.cr" require "./std/io/hexdump_spec.cr" require "./std/io/io_spec.cr" require "./std/io/memory_spec.cr" @@ -111,7 +107,6 @@ require "./std/iterator_spec.cr" require "./std/json/any_spec.cr" require "./std/json/builder_spec.cr" require "./std/json/lexer_spec.cr" -require "./std/json/mapping_spec.cr" require "./std/json/parser_spec.cr" require "./std/json/pull_parser_spec.cr" require "./std/json/serializable_spec.cr" @@ -131,7 +126,6 @@ require "./std/log/env_config_spec.cr" require "./std/log/io_backend_spec.cr" require "./std/log/log_spec.cr" require "./std/log/main_spec.cr" -require "./std/logger_spec.cr" require "./std/match_data_spec.cr" # require "./std/math_spec.cr" (failed linking) require "./std/mime/media_type_spec.cr" @@ -221,15 +215,15 @@ require "./std/uint_spec.cr" require "./std/uri/punycode_spec.cr" require "./std/uri_spec.cr" require "./std/uuid_spec.cr" +# require "./std/va_list_spec.cr" require "./std/weak_ref_spec.cr" -# require "./std/xml/builder_spec.cr" (failed linking) -# require "./std/xml/html_spec.cr" (failed linking) -# require "./std/xml/reader_spec.cr" (failed linking) -# require "./std/xml/xml_spec.cr" (failed linking) -# require "./std/xml/xpath_spec.cr" (failed linking) +require "./std/xml/builder_spec.cr" +require "./std/xml/html_spec.cr" +require "./std/xml/reader_spec.cr" +require "./std/xml/xml_spec.cr" +require "./std/xml/xpath_spec.cr" require "./std/yaml/any_spec.cr" require "./std/yaml/builder_spec.cr" -require "./std/yaml/mapping_spec.cr" require "./std/yaml/nodes/builder_spec.cr" require "./std/yaml/schema/core_spec.cr" require "./std/yaml/schema/fail_safe_spec.cr" @@ -237,8 +231,3 @@ require "./std/yaml/serializable_spec.cr" require "./std/yaml/serialization_spec.cr" require "./std/yaml/yaml_pull_parser_spec.cr" require "./std/yaml/yaml_spec.cr" -require "./std/zip/zip_file_spec.cr" -require "./std/zip/zip_spec.cr" -require "./std/zlib/reader_spec.cr" -require "./std/zlib/stress_spec.cr" -require "./std/zlib/writer_spec.cr" diff --git a/src/VERSION b/src/VERSION index cd748687dc99..05639a55677c 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -0.35.0-dev +1.0.0-dev diff --git a/src/adler32/adler32.cr b/src/adler32/adler32.cr deleted file mode 100644 index 43a9737d332d..000000000000 --- a/src/adler32/adler32.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "digest" - -module Adler32 - @[Deprecated("Use `Digest::Adler32.initial` instead")] - def self.initial : UInt32 - Digest::Adler32.initial - end - - @[Deprecated("Use `Digest::Adler32.checksum` instead")] - def self.checksum(data) : UInt32 - Digest::Adler32.checksum(data) - end - - @[Deprecated("Use `Digest::Adler32.update` instead")] - def self.update(data, adler32 : UInt32) : UInt32 - Digest::Adler32.update(data, adler32) - end - - @[Deprecated("Use `Digest::Adler32.combine` instead")] - def self.combine(adler1 : UInt32, adler2 : UInt32, len) : UInt32 - Digest::Adler32.combine(adler1, adler2, len) - end -end diff --git a/src/annotations.cr b/src/annotations.cr index fcf2d7c26e7c..c906f61b9b79 100644 --- a/src/annotations.cr +++ b/src/annotations.cr @@ -27,7 +27,7 @@ end annotation Flags end -# A `lib` can be marked with `@[Link(lib : String, ldflags : String, static : Bool, framework : String)]` +# A `lib` can be marked with `@[Link(lib : String, *, ldflags : String, framework : String, pkg_config : String)]` # to declare the library that should be linked when compiling the program. # # At least one of the *lib*, *ldflags*, *framework* arguments needs to be specified. @@ -38,16 +38,29 @@ end # 1. will lookup `pcre` using `pkg-config`, if not found # 2. will pass `-lpcre` to the linker. # -# `@[Link("pcre", static: true)]` will favor static libraries over shared libraries. -# 1. will lookup `libpcre.a` in `CRYSTAL_LIBRARY_PATH`, if not found -# 2. will lookup `pcre` using `pkg-config --static`, if not found, -# 3. will lookup `libpcre.a` in `/usr/lib`, `/usr/local/lib` +# `@[Link("pcre", pkg_config: "libpcre")]` will lookup for a shared library. +# 1. will lookup `libpcre` using `pkg-config`, if not found +# 2. will lookup `pcre` using `pkg-config`, if not found +# 3. will pass `-lpcre` to the linker. # # `@[Link(framework: "Cocoa")]` will pass `-framework Cocoa` to the linker. # # When an `-l` option is passed to the linker, it will lookup the libraries in -# paths passed with the `-L` option. `CRYSTAL_LIBRARY_PATH`, `/usr/lib`, -# and `/usr/local/lib` are added by default. Custom paths can be passed -# using `ldflags`: `@[Link(ldflags: "-Lvendor/bin")]`. +# paths passed with the `-L` option. Any paths in `CRYSTAL_LIBRARY_PATH` are +# added by default. Custom paths can be passed using `ldflags`: +# `@[Link(ldflags: "-Lvendor/bin")]`. annotation Link end + +# This annotation marks methods, classes, constants, and macros as experimental. +# +# Experimental features are subject to change or be removed despite the +# [https://semver.org/](https://semver.org/) guarantees. +# +# ``` +# @[Experimental("Join discussion about this topic at ...")] +# def foo +# end +# ``` +annotation Experimental +end diff --git a/src/array.cr b/src/array.cr index 9729a255ed86..997004d422b1 100644 --- a/src/array.cr +++ b/src/array.cr @@ -47,8 +47,7 @@ class Array(T) include Indexable(T) include Comparable(Array) - # Size of an Array that we consider small to do linear scans - # or other optimizations instead of using a lookup Hash. + # Size of an Array that we consider small to do linear scans or other optimizations. private SMALL_ARRAY_SIZE = 16 # Returns the number of elements in the array. @@ -751,6 +750,11 @@ class Array(T) # a.delete_at(99, 1) # raises IndexError # ``` def delete_at(index : Int, count : Int) + index += size if index < 0 + unless 0 <= index <= size + raise IndexError.new + end + val = self[index, count] count = index + count <= size ? count : size - index (@buffer + index).move_from(@buffer + index + count, size - index - count) @@ -1596,11 +1600,45 @@ class Array(T) self end + # Returns `self` with all the elements shifted `n` times. + # + # ``` + # a1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # a2 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # a3 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # + # a1.rotate! + # a2.rotate!(1) + # a3.rotate!(3) + # + # a1 # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + # a2 # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + # a3 # => [3, 4, 5, 6, 7, 8, 9, 0, 1, 2] + # ``` def rotate!(n = 1) return self if size == 0 n %= size - return self if n == 0 - if n <= size // 2 + + if n == 0 + elsif n == 1 + tmp = self[0] + @buffer.move_from(@buffer + n, size - n) + self[-1] = tmp + elsif n == (size - 1) + tmp = self[-1] + (@buffer + size - n).move_from(@buffer, n) + self[0] = tmp + elsif n <= SMALL_ARRAY_SIZE + tmp_buffer = uninitialized StaticArray(T, SMALL_ARRAY_SIZE) + tmp_buffer.to_unsafe.copy_from(@buffer, n) + @buffer.move_from(@buffer + n, size - n) + (@buffer + size - n).copy_from(tmp_buffer.to_unsafe, n) + elsif size - n <= SMALL_ARRAY_SIZE + tmp_buffer = uninitialized StaticArray(T, SMALL_ARRAY_SIZE) + tmp_buffer.to_unsafe.copy_from(@buffer + n, size - n) + (@buffer + size - n).move_from(@buffer, n) + @buffer.copy_from(tmp_buffer.to_unsafe, size - n) + elsif n <= size // 2 tmp = self[0..n] @buffer.move_from(@buffer + n, size - n) (@buffer + size - n).copy_from(tmp.to_unsafe, n) @@ -1612,6 +1650,15 @@ class Array(T) self end + # Returns an array with all the elements shifted `n` times. + # + # ``` + # a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # a.rotate # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + # a.rotate(1) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + # a.rotate(3) # => [3, 4, 5, 6, 7, 8, 9, 0, 1, 2] + # a # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + # ``` def rotate(n = 1) return self if size == 0 n %= size @@ -1875,7 +1922,7 @@ class Array(T) def to_s(io : IO) : Nil executed = exec_recursive(:to_s) do io << '[' - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << ']' end io << "[...]" unless executed diff --git a/src/big/big_int.cr b/src/big/big_int.cr index 18e9f824fc95..04fa1e142b03 100644 --- a/src/big/big_int.cr +++ b/src/big/big_int.cr @@ -433,14 +433,20 @@ struct BigInt < Int # BigInt.new("123456789101101987654321").to_s(36) # => "k3qmt029k48nmpd" # ``` def to_s(base : Int) : String - raise "Invalid base #{base}" unless 2 <= base <= 36 + raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 cstr = LibGMP.get_str(nil, base, self) String.new(cstr) end - def digits : Array(Int32) + # :nodoc: + def digits(base = 10) : Array(Int32) + if self < 0 + raise ArgumentError.new("Can't request digits of negative number") + end + ary = [] of Int32 - self.to_s.each_char { |c| ary << c - '0' } + self.to_s(base).each_char { |c| ary << c.to_i(base) } + ary.reverse! ary end diff --git a/src/bit_array.cr b/src/bit_array.cr index ef779b458583..48578c259955 100644 --- a/src/bit_array.cr +++ b/src/bit_array.cr @@ -38,10 +38,6 @@ struct BitArray return LibC.memcmp(@bits, other.@bits, malloc_size) == 0 end - def ==(other) - false - end - def unsafe_fetch(index : Int) bit_index, sub_index = index.divmod(32) (@bits[bit_index] & (1 << sub_index)) > 0 @@ -243,6 +239,13 @@ struct BitArray hasher end + # Returns a new `BitArray` with all of the same elements. + def dup + bit_array = BitArray.new(@size) + @bits.copy_to(bit_array.@bits, malloc_size) + bit_array + end + private def bit_index_and_sub_index(index) bit_index_and_sub_index(index) { raise IndexError.new } end diff --git a/src/channel.cr b/src/channel.cr index 83ea9d6c238f..3d6a47122a8c 100644 --- a/src/channel.cr +++ b/src/channel.cr @@ -26,9 +26,12 @@ class Channel(T) @lock = Crystal::SpinLock.new @queue : Deque(T)? + # :nodoc: record NotReady + # :nodoc: record UseDefault + # :nodoc: module SelectAction(S) abstract def execute : DeliveryState abstract def wait(context : SelectContext(S)) @@ -72,7 +75,7 @@ class Channel(T) end end - enum SelectState + private enum SelectState None = 0 Active = 1 Done = 2 @@ -117,7 +120,7 @@ class Channel(T) end end - enum DeliveryState + private enum DeliveryState None Delivered Closed @@ -183,11 +186,14 @@ class Channel(T) # Both awaiting and subsequent calls to `#send` will consider the channel closed. # All items successfully sent to the channel can be received, before `#receive` considers the channel closed. # Calling `#close` on a closed channel does not have any effect. - def close : Nil + # + # It returns `true` when the channel was successfully closed, or `false` if it was already closed. + def close : Bool sender_list = Crystal::PointerLinkedList(Sender(T)).new receiver_list = Crystal::PointerLinkedList(Receiver(T)).new @lock.sync do + return false if @closed @closed = true @senders, sender_list = sender_list, @senders @@ -196,6 +202,7 @@ class Channel(T) sender_list.each(&.value.close) receiver_list.each(&.value.close) + true end def closed? @@ -269,7 +276,7 @@ class Channel(T) # end # channel.receive # => 1 # ``` - def receive + def receive : T receive_impl { raise ClosedError.new } end @@ -277,11 +284,11 @@ class Channel(T) # If there is a value waiting, it is returned immediately. Otherwise, this method blocks until a value is sent to the channel. # # Returns `nil` if the channel is closed or closes while waiting for receive. - def receive? + def receive? : T? receive_impl { return nil } end - def receive_impl + private def receive_impl receiver = Receiver(T).new @lock.lock @@ -314,7 +321,7 @@ class Channel(T) end end - def receive_internal + protected def receive_internal if (queue = @queue) && !queue.empty? deque_value = queue.shift if sender_ptr = dequeue_sender @@ -391,34 +398,29 @@ class Channel(T) nil end + # :nodoc: def self.select(*ops : SelectAction) self.select ops end + # :nodoc: def self.select(ops : Indexable(SelectAction)) i, m = select_impl(ops, false) raise "BUG: blocking select returned not ready status" if m.is_a?(NotReady) return i, m end - @[Deprecated("Use Channel.non_blocking_select")] - def self.select(ops : Indexable(SelectAction), has_else) - # The overload of Channel.select(Indexable(SelectAction), Bool) - # is used by LiteralExpander with the second argument as `true`. - # This overload is kept as a transition, but 0.32 will emit calls to - # Channel.select or Channel.non_blocking_select directly - non_blocking_select(ops) - end - + # :nodoc: def self.non_blocking_select(*ops : SelectAction) self.non_blocking_select ops end + # :nodoc: def self.non_blocking_select(ops : Indexable(SelectAction)) select_impl(ops, true) end - def self.select_impl(ops : Indexable(SelectAction), non_blocking) + private def self.select_impl(ops : Indexable(SelectAction), non_blocking) # Sort the operations by the channel they contain # This is to avoid deadlocks between concurrent `select` calls ops_locks = ops @@ -488,8 +490,7 @@ class Channel(T) LooseReceiveAction.new(self) end - # :nodoc: - class StrictReceiveAction(T) + private class StrictReceiveAction(T) include SelectAction(T) property receiver : Receiver(T) @@ -553,8 +554,7 @@ class Channel(T) end end - # :nodoc: - class LooseReceiveAction(T) + private class LooseReceiveAction(T) include SelectAction(T) property receiver : Receiver(T) @@ -618,8 +618,7 @@ class Channel(T) end end - # :nodoc: - class SendAction(T) + private class SendAction(T) include SelectAction(Nil) property sender : Sender(T) diff --git a/src/char.cr b/src/char.cr index e450d4becd7f..c0a184010669 100644 --- a/src/char.cr +++ b/src/char.cr @@ -477,7 +477,7 @@ struct Char dump_or_inspect do |io| if ascii_control? io << "\\u{" - ord.to_s(16, io) + ord.to_s(io, 16) io << '}' else to_s(io) @@ -505,7 +505,7 @@ struct Char dump_or_inspect do |io| if ascii_control? || ord >= 0x80 io << "\\u{" - ord.to_s(16, io) + ord.to_s(io, 16) io << '}' else to_s(io) diff --git a/src/char/reader.cr b/src/char/reader.cr index 6673c814bc20..edc0450b2bf3 100644 --- a/src/char/reader.cr +++ b/src/char/reader.cr @@ -185,65 +185,63 @@ struct Char end private def decode_char_at(pos) - first = byte_at?(pos) || 0u32 + first = byte_at(pos) if first < 0x80 return yield first, 1, nil end if first < 0xc2 - invalid_byte_sequence 1 + invalid_byte_sequence end - second = byte_at?(pos + 1) - if second.nil? || (second & 0xc0) != 0x80 - invalid_byte_sequence 1 + second = byte_at(pos + 1) + if (second & 0xc0) != 0x80 + invalid_byte_sequence end if first < 0xe0 return yield (first << 6) &+ (second &- 0x3080), 2, nil end - third = byte_at?(pos + 2) - if third.nil? || (third & 0xc0) != 0x80 - invalid_byte_sequence 2 + third = byte_at(pos + 2) + if (third & 0xc0) != 0x80 + invalid_byte_sequence end if first < 0xf0 if first == 0xe0 && second < 0xa0 - invalid_byte_sequence 3 + invalid_byte_sequence end if first == 0xed && second >= 0xa0 - invalid_byte_sequence 3 + invalid_byte_sequence end return yield (first << 12) &+ (second << 6) &+ (third &- 0xE2080), 3, nil end if first == 0xf0 && second < 0x90 - invalid_byte_sequence 3 + invalid_byte_sequence end if first == 0xf4 && second >= 0x90 - invalid_byte_sequence 3 + invalid_byte_sequence end - fourth = byte_at?(pos + 3) - if fourth.nil? - invalid_byte_sequence 3 - elsif (fourth & 0xc0) != 0x80 - invalid_byte_sequence 4 + fourth = byte_at(pos + 3) + if (fourth & 0xc0) != 0x80 + invalid_byte_sequence end if first < 0xf5 return yield (first << 18) &+ (second << 12) &+ (third << 6) &+ (fourth &- 0x3C82080), 4, nil end - invalid_byte_sequence 4 + invalid_byte_sequence end - private macro invalid_byte_sequence(width) - return yield Char::REPLACEMENT.ord, {{width}}, first.to_u8 + private macro invalid_byte_sequence + return yield Char::REPLACEMENT.ord, 1, first.to_u8 end @[AlwaysInline] @@ -274,11 +272,7 @@ struct Char end private def byte_at(i) - @string.byte_at(i).to_u32 - end - - private def byte_at?(i) - @string.byte_at?(i).try(&.to_u32) + @string.to_unsafe[i].to_u32 end end end diff --git a/src/colorize.cr b/src/colorize.cr index 1d6bf38e0a0e..792faa55e229 100644 --- a/src/colorize.cr +++ b/src/colorize.cr @@ -150,16 +150,6 @@ module Colorize end end -@[Deprecated("Use `Colorize.with`")] -def with_color - Colorize.with -end - -@[Deprecated("Use `Colorize.with.fore(color)`")] -def with_color(color : Symbol) - Colorize.with.fore(color) -end - module Colorize::ObjectExtensions def colorize Colorize::Object.new(self) @@ -224,12 +214,16 @@ module Colorize blue : UInt8 do def fore(io : IO) : Nil io << "38;2;" - {red, green, blue}.join(';', io, &.to_s io) + io << red << ";" + io << green << ";" + io << blue end def back(io : IO) : Nil io << "48;2;" - {red, green, blue}.join(';', io, &.to_s io) + io << red << ";" + io << green << ";" + io << blue end end end diff --git a/src/compiler/crystal.cr b/src/compiler/crystal.cr index beefe336ffb1..1d14ec386bc6 100644 --- a/src/compiler/crystal.cr +++ b/src/compiler/crystal.cr @@ -6,6 +6,6 @@ require "log" require "./crystal/**" -Log.setup_from_env(default_level: :warning, default_sources: "crystal.*") +Log.setup_from_env(default_level: :warn, default_sources: "crystal.*") Crystal::Command.run diff --git a/src/compiler/crystal/annotatable.cr b/src/compiler/crystal/annotatable.cr new file mode 100644 index 000000000000..0459a912c2b3 --- /dev/null +++ b/src/compiler/crystal/annotatable.cr @@ -0,0 +1,23 @@ +module Crystal + module Annotatable + # Annotations on this instance + property annotations : Hash(AnnotationType, Array(Annotation))? + + # Adds an annotation with the given type and value + def add_annotation(annotation_type : AnnotationType, value : Annotation) + annotations = @annotations ||= {} of AnnotationType => Array(Annotation) + annotations[annotation_type] ||= [] of Annotation + annotations[annotation_type] << value + end + + # Returns the last defined annotation with the given type, if any, or `nil` otherwise + def annotation(annotation_type) : Annotation? + @annotations.try &.[annotation_type]?.try &.last? + end + + # Returns all annotations with the given type, if any, or `nil` otherwise + def annotations(annotation_type) : Array(Annotation)? + @annotations.try &.[annotation_type]? + end + end +end diff --git a/src/compiler/crystal/codegen/cache_dir.cr b/src/compiler/crystal/codegen/cache_dir.cr index 72aca6ca13da..ea0184a3f1d9 100644 --- a/src/compiler/crystal/codegen/cache_dir.cr +++ b/src/compiler/crystal/codegen/cache_dir.cr @@ -119,7 +119,7 @@ module Crystal .sort_by! { |dir| File.info?(dir).try(&.modification_time) || Time.unix(0) } .reverse! .skip(10) - .each { |name| `rm -rf "#{name}"` rescue nil } + .each { |name| `rm -rf -- #{Process.quote(name)}` rescue nil } end private def gather_cache_entries(dir) diff --git a/src/compiler/crystal/codegen/call.cr b/src/compiler/crystal/codegen/call.cr index 78cadf4b39ac..65f58123d89c 100644 --- a/src/compiler/crystal/codegen/call.cr +++ b/src/compiler/crystal/codegen/call.cr @@ -16,8 +16,6 @@ class Crystal::CodeGenVisitor return false end - check_call_to_deprecated_method node - owner = node.name == "super" ? node.scope : node.target_def.owner call_args, has_out = prepare_call_args node, owner @@ -27,7 +25,7 @@ class Crystal::CodeGenVisitor return false if @builder.end if block = node.block - # A block might turn into a proc literal but not be used if it particpates in a dispatch + # A block might turn into a proc literal but not be used if it participates in a dispatch if (fun_literal = block.fun_literal) && node.target_def.uses_block_arg? codegen_call_with_block_as_fun_literal(node, fun_literal, owner, call_args) else @@ -168,6 +166,8 @@ class Crystal::CodeGenVisitor call_args << sret_value end + target_def_args_size = target_def.args.size + node.args.each_with_index do |arg, i| if arg.is_a?(Out) has_out = true @@ -219,13 +219,26 @@ class Crystal::CodeGenVisitor case abi_arg_type.kind when LLVM::ABI::ArgKind::Direct call_arg = codegen_direct_abi_call(call_arg, abi_arg_type) unless arg.type.nil_type? - call_args << call_arg when LLVM::ABI::ArgKind::Indirect # Pass argument as is (will be passed byval) - call_args << call_arg when LLVM::ABI::ArgKind::Ignore # Ignore + next end + + # If we are passing variadic arguments there are some special rules + if i >= target_def_args_size + arg_type = arg.type.remove_indirection + if arg_type.is_a?(FloatType) && arg_type.kind == :f32 + # Floats must be passed as doubles (there are no float varargs) + call_arg = extend_float @program.float64, call_arg + elsif arg_type.is_a?(IntegerType) && arg_type.kind.in?(:i8, :u8, :i16, :u16) + # Integer with a size less that `int` must be converted to `int` + call_arg = extend_int arg_type, @program.int32, call_arg + end + end + + call_args << call_arg end @needs_value = old_needs_value diff --git a/src/compiler/crystal/codegen/codegen.cr b/src/compiler/crystal/codegen/codegen.cr index 41617ee584d5..3a47dcc74c52 100644 --- a/src/compiler/crystal/codegen/codegen.cr +++ b/src/compiler/crystal/codegen/codegen.cr @@ -563,7 +563,7 @@ module Crystal end def fun_literal_name(node : ProcLiteral) - location = node.location.try &.original_location + location = node.location.try &.expanded_location if location && (type = node.type?) proc_name = true filename = location.filename.as(String) @@ -1187,6 +1187,7 @@ module Crystal end def read_instance_var(node_type, type, name, value) + type = type.remove_typedef ivar = type.lookup_instance_var(name) ivar_ptr = instance_var_ptr type, name, value @last = downcast ivar_ptr, node_type, ivar.type, false @@ -1305,7 +1306,7 @@ module Crystal ] of ASTNode if location = node.location - pieces << StringLiteral.new(", at #{location.original_location}:#{location.line_number}").at(node) + pieces << StringLiteral.new(", at #{location.expanded_location}:#{location.line_number}").at(node) end ex = Call.new(Path.global("TypeCastError").at(node), "new", StringInterpolation.new(pieces).at(node)).at(node) @@ -1324,6 +1325,7 @@ module Crystal location = Location.new(@program.filename, 1, 1) call = Call.global("raise", StringLiteral.new("passing a closure to C is not allowed")).at(location) @program.visit_main call + call.raise "::raise must be of NoReturn return type!" unless call.type.is_a?(NoReturnType) call end end @@ -2035,7 +2037,7 @@ module Crystal if raise_overflow_fun = @raise_overflow_fun check_main_fun RAISE_OVERFLOW_NAME, raise_overflow_fun else - raise "BUG: __crystal_raise_overflow is not defined" + raise LocationlessException.new("Missing __crystal_raise_overflow function, either use std-lib's prelude or define it") end end @@ -2209,7 +2211,7 @@ module Crystal str << char else str << '.' - char.ord.to_s(16, str, upcase: true) + char.ord.to_s(str, 16, upcase: true) str << '.' end end diff --git a/src/compiler/crystal/codegen/const.cr b/src/compiler/crystal/codegen/const.cr index 893c18a52b43..e64c6e3d98fb 100644 --- a/src/compiler/crystal/codegen/const.cr +++ b/src/compiler/crystal/codegen/const.cr @@ -96,9 +96,43 @@ class Crystal::CodeGenVisitor end end + def initialize_no_init_flag_const(const) + global = declare_const(const) + + with_cloned_context do + # "self" in a constant is the constant's namespace + context.type = const.namespace + + # Start with fresh variables + context.vars = LLVMVars.new + + alloca_vars const.vars + request_value do + accept const.value + end + end + + const_type = const.value.type + if const_type.passed_by_value? + @last = load @last + end + + store @last, global + + global.initializer = @last.type.null + + global + end + def initialize_const(const) - # Maybe the constant was simple and doesn't need a real initialization + # If the constant wasn't read yet, we can initialize it right now and + # avoid checking an "initialized" flag every time we read it. + unless const.read? + const.no_init_flag = true + return initialize_no_init_flag_const(const) + end + # Maybe the constant was simple and doesn't need a real initialization global, initialized_flag = declare_const_and_initialized_flag(const) return global if const.initializer @@ -173,7 +207,9 @@ class Crystal::CodeGenVisitor end def read_const_pointer(const) - if const == @program.argc || const == @program.argv || const.initializer + const.read = true + + if const == @program.argc || const == @program.argv || const.initializer || const.no_init_flag? global_name = const.llvm_name global = declare_const(const) diff --git a/src/compiler/crystal/codegen/debug.cr b/src/compiler/crystal/codegen/debug.cr index d3c978d04635..408aaf13069c 100644 --- a/src/compiler/crystal/codegen/debug.cr +++ b/src/compiler/crystal/codegen/debug.cr @@ -318,7 +318,7 @@ module Crystal end private def declare_local(type, alloca, location, basic_block : LLVM::BasicBlock? = nil) - location = location.try &.original_location + location = location.try &.expanded_location return false unless location file, dir = file_and_dir(location.filename) @@ -435,7 +435,7 @@ module Crystal end def set_current_debug_location(location) - location = location.try &.original_location + location = location.try &.expanded_location return unless location @current_debug_location = location @@ -464,7 +464,7 @@ module Crystal end def emit_def_debug_metadata(target_def) - location = target_def.location.try &.original_location + location = target_def.location.try &.expanded_location return unless location file, dir = file_and_dir(location.filename) diff --git a/src/compiler/crystal/codegen/experimental.cr b/src/compiler/crystal/codegen/experimental.cr new file mode 100644 index 000000000000..3af1a6281b18 --- /dev/null +++ b/src/compiler/crystal/codegen/experimental.cr @@ -0,0 +1,34 @@ +module Crystal + struct ExperimentalAnnotation + getter message : String? + + def initialize(@message = nil) + end + + def self.from(ann : Annotation) + args = ann.args + named_args = ann.named_args + + if named_args + ann.raise "too many named arguments (given #{named_args.size}, expected maximum 0)" + end + + message = nil + count = 0 + + args.each do |arg| + case count + when 0 + arg.raise "first argument must be a String" unless arg.is_a?(StringLiteral) + message = arg.value + else + ann.wrong_number_of "experimental annotation arguments", args.size, "1" + end + + count += 1 + end + + new(message) + end + end +end diff --git a/src/compiler/crystal/codegen/fun.cr b/src/compiler/crystal/codegen/fun.cr index 07cea2419a01..8562cacb4528 100644 --- a/src/compiler/crystal/codegen/fun.cr +++ b/src/compiler/crystal/codegen/fun.cr @@ -88,8 +88,12 @@ class Crystal::CodeGenVisitor context.fun.add_attribute LLVM::Attribute::UWTable if @program.has_flag?("darwin") # Disable frame pointer elimination in Darwin, as it causes issues during stack unwind - context.fun.add_target_dependent_attribute "no-frame-pointer-elim", "true" - context.fun.add_target_dependent_attribute "no-frame-pointer-elim-non-leaf", "true" + {% if compare_versions(Crystal::LLVM_VERSION, "8.0.0") < 0 %} + context.fun.add_target_dependent_attribute "no-frame-pointer-elim", "true" + context.fun.add_target_dependent_attribute "no-frame-pointer-elim-non-leaf", "true" + {% else %} + context.fun.add_target_dependent_attribute "frame-pointer", "all" + {% end %} end new_entry_block @@ -512,12 +516,23 @@ class Crystal::CodeGenVisitor context.vars[arg.name] = LLVMVar.new(value, var_type) return else + # If an argument is a Proc inside a C function, we need to cast it to Proc + fun_proc = var_type.is_a?(ProcInstanceType) && target_def.is_a?(External) + # We don't need to create a copy of the argument if it's never # assigned a value inside the function. needs_copy = target_def_var.try &.assigned_to? + needs_copy ||= fun_proc + if needs_copy pointer = alloca(llvm_type(var_type), arg.name) pointer = declare_debug_for_function_argument(arg.name, var_type, index + 1, pointer, location) unless target_def.naked? + + if fun_proc + value = bit_cast(value, llvm_context.void_pointer) + value = make_fun(var_type, value, llvm_context.void_pointer.null) + end + context.vars[arg.name] = LLVMVar.new(pointer, var_type) if arg.type.passed_by_value? && !context.fun.attributes(index + 1).by_val? diff --git a/src/compiler/crystal/codegen/link.cr b/src/compiler/crystal/codegen/link.cr index cf3f381f806d..9eea3ae82fde 100644 --- a/src/compiler/crystal/codegen/link.cr +++ b/src/compiler/crystal/codegen/link.cr @@ -1,10 +1,11 @@ module Crystal struct LinkAnnotation getter lib : String? + getter pkg_config : String? getter ldflags : String? getter framework : String? - def initialize(@lib = nil, @ldflags = nil, @static = false, @framework = nil) + def initialize(@lib = nil, @pkg_config = @lib, @ldflags = nil, @static = false, @framework = nil) end def static? @@ -22,6 +23,7 @@ module Crystal lib_name = nil lib_ldflags = nil lib_static = false + lib_pkg_config = nil lib_framework = nil count = 0 @@ -66,12 +68,15 @@ module Crystal named_arg.raise "'framework' link argument already specified" if count > 3 named_arg.raise "'framework' link argument must be a String" unless value.is_a?(StringLiteral) lib_framework = value.value + when "pkg_config" + named_arg.raise "'pkg_config' link argument must be a String" unless value.is_a?(StringLiteral) + lib_pkg_config = value.value else - named_arg.raise "unknown link argument: '#{named_arg.name}' (valid arguments are 'lib', 'ldflags', 'static' and 'framework')" + named_arg.raise "unknown link argument: '#{named_arg.name}' (valid arguments are 'lib', 'ldflags', 'static', 'pkg_config' and 'framework')" end end - new(lib_name, lib_ldflags, lib_static, lib_framework) + new(lib_name, lib_pkg_config, lib_ldflags, lib_static, lib_framework) end end @@ -86,6 +91,10 @@ module Crystal end class Program + def object_extension + has_flag?("windows") ? ".obj" : ".o" + end + def lib_flags has_flag?("windows") ? lib_flags_windows : lib_flags_posix end @@ -98,90 +107,75 @@ module Crystal end if libname = ann.lib - flags << ' ' << libname << ".lib" + flags << ' ' << Process.quote_windows("#{libname}.lib") end end end end private def lib_flags_posix - library_path = ENV["LIBRARY_PATH"]?.try(&.split(':', remove_empty: true)) || - ["/usr/lib", "/usr/local/lib"] - has_pkg_config = nil + flags = [] of String + static_build = has_flag?("static") - String.build do |flags| - link_annotations.reverse_each do |ann| - if ldflags = ann.ldflags - flags << ' ' << ldflags - end + # Instruct the linker to link statically if the user asks + flags << "-static" if static_build - if libname = ann.lib - if has_pkg_config.nil? - has_pkg_config = Process.run("pkg-config", ["-h"]).success? - end - - static = has_flag?("static") || ann.static? - - if static && (static_lib = find_static_lib(libname, CrystalLibraryPath.paths)) - flags << ' ' << static_lib - elsif has_pkg_config && (libflags = pkg_config_flags(libname, static, library_path)) - flags << ' ' << libflags - elsif static && (static_lib = find_static_lib(libname, library_path)) - flags << ' ' << static_lib - else - flags << " -l" << libname - end - end + # Add CRYSTAL_LIBRARY_PATH locations, so the linker preferentially + # searches user-given library paths. + CrystalLibraryPath.paths.each do |path| + flags << Process.quote_posix("-L#{path}") + end - if framework = ann.framework - flags << " -framework " << framework - end + link_annotations.reverse_each do |ann| + if ldflags = ann.ldflags + flags << ldflags end - # Append the CRYSTAL_LIBRARY_PATH values as -L flags. - CrystalLibraryPath.paths.each do |path| - flags << " -L#{path}" + # First, check pkg-config for the pkg-config module name if provided, then + # check pkg-config with the lib name, then fall back to -lname + if (pkg_config_name = ann.pkg_config) && (flag = pkg_config(pkg_config_name, static_build)) + flags << flag + elsif (lib_name = ann.lib) && (flag = pkg_config(lib_name, static_build)) + flags << flag + elsif (lib_name = ann.lib) + flags << Process.quote_posix("-l#{lib_name}") end - # Append the default paths as -L flags in case the linker doesn't know - # about them (eg: FreeBSD won't search /usr/local/lib by default): - library_path.each do |path| - flags << " -L#{path}" + + if framework = ann.framework + flags << "-framework" << Process.quote_posix(framework) end end - end - def link_annotations - annotations = [] of LinkAnnotation - add_link_annotations @types, annotations - annotations + flags.join(" ") end - private def pkg_config_flags(libname, static, library_path) - if system("pkg-config #{libname}") - if static - flags = [] of String - `pkg-config #{libname} --libs --static`.split.each do |cfg| - if cfg.starts_with?("-L") - library_path << cfg[2..-1] - elsif cfg.starts_with?("-l") - flags << (find_static_lib(cfg[2..-1], library_path) || cfg) - else - flags << cfg - end - end - flags.join ' ' - else - `pkg-config #{libname} --libs`.chomp - end + PKG_CONFIG_PATH = Process.find_executable("pkg-config") + + # Returns the result of running `pkg-config mod` but returns nil if + # pkg-config is not installed, or the module does not exist. + private def pkg_config(mod, static = false) : String? + return unless pkg_config_path = PKG_CONFIG_PATH + return unless Process.run(pkg_config_path, {mod}).success? + + args = ["--libs"] + args << "--static" if static + args << mod + + process = Process.new(pkg_config_path, args, input: :close, output: :pipe, error: :inherit) + flags = process.output.gets_to_end.chomp + status = process.wait + if status.success? + flags + else + nil end end - private def find_static_lib(libname, library_path) - library_path.each do |libdir| - static_lib = "#{libdir}/lib#{libname}.a" - return static_lib if File.exists?(static_lib) - end - nil + # Returns every @[Link] annotation in the program parsed as `LinkAnnotation` + def link_annotations + annotations = [] of LinkAnnotation + add_link_annotations @types, annotations + annotations end private def add_link_annotations(types, annotations) diff --git a/src/compiler/crystal/codegen/primitives.cr b/src/compiler/crystal/codegen/primitives.cr index 39bfe6d53836..5c7a00bd8bcd 100644 --- a/src/compiler/crystal/codegen/primitives.cr +++ b/src/compiler/crystal/codegen/primitives.cr @@ -131,12 +131,15 @@ class Crystal::CodeGenVisitor else # go on end + case op + when "+", "-", "*" + return codegen_binary_op_with_overflow(op, t1, t2, p1, p2) + else # go on + end + tmax, p1, p2 = codegen_binary_extend_int(t1, t2, p1, p2) case op - when "+" then codegen_binary_op_add(tmax, t1, t2, p1, p2) - when "-" then codegen_binary_op_sub(tmax, t1, t2, p1, p2) - when "*" then codegen_binary_op_mul(tmax, t1, t2, p1, p2) when "&+" then codegen_trunc_binary_op_result(t1, t2, builder.add(p1, p2)) when "&-" then codegen_trunc_binary_op_result(t1, t2, builder.sub(p1, p2)) when "&*" then codegen_trunc_binary_op_result(t1, t2, builder.mul(p1, p2)) @@ -151,6 +154,79 @@ class Crystal::CodeGenVisitor end end + def codegen_binary_op_with_overflow(op, t1, t2, p1, p2) + if op == "*" + if t1.unsigned? && t2.signed? + return codegen_mul_unsigned_signed_with_overflow(t1, t2, p1, p2) + elsif t1.signed? && t2.unsigned? + return codegen_mul_signed_unsigned_with_overflow(t1, t2, p1, p2) + end + end + + calc_signed = t1.signed? || t2.signed? + calc_width = {t1, t2}.map { |t| t.bytes * 8 + ((calc_signed && t.unsigned?) ? 1 : 0) }.max + calc_type = llvm_context.int(calc_width) + + e1 = t1.signed? ? builder.sext(p1, calc_type) : builder.zext(p1, calc_type) + e2 = t2.signed? ? builder.sext(p2, calc_type) : builder.zext(p2, calc_type) + + llvm_op = + case {calc_signed, op} + when {false, "+"} then "uadd" + when {false, "-"} then "usub" + when {false, "*"} then "umul" + when {true, "+"} then "sadd" + when {true, "-"} then "ssub" + when {true, "*"} then "smul" + else raise "BUG: unknown overflow op" + end + + llvm_fun = binary_overflow_fun "llvm.#{llvm_op}.with.overflow.i#{calc_width}", calc_type + res_with_overflow = builder.call(llvm_fun, [e1, e2]) + + result = extract_value res_with_overflow, 0 + overflow = extract_value res_with_overflow, 1 + + if calc_width > t1.bytes * 8 + result_trunc = trunc result, llvm_type(t1) + result_trunc_ext = t1.signed? ? builder.sext(result_trunc, calc_type) : builder.zext(result_trunc, calc_type) + overflow = or(overflow, builder.icmp LLVM::IntPredicate::NE, result, result_trunc_ext) + end + + codegen_raise_overflow_cond overflow + + trunc result, llvm_type(t1) + end + + def codegen_mul_unsigned_signed_with_overflow(t1, t2, p1, p2) + overflow = and( + codegen_binary_op_ne(t1, t1, p1, int(0, t1)), # self != 0 + codegen_binary_op_lt(t2, t2, p2, int(0, t2)) # other < 0 + ) + codegen_raise_overflow_cond overflow + + return codegen_binary_op_with_overflow("*", t1, @program.int_type(false, t2.bytes), p1, p2) + end + + def codegen_mul_signed_unsigned_with_overflow(t1, t2, p1, p2) + negative = codegen_binary_op_lt(t1, t1, p1, int(0, t1)) # self < 0 + minus_p1 = builder.sub int(0, t1), p1 + abs = builder.select negative, minus_p1, p1 + u1 = @program.int_type(false, t1.bytes) + + # tmp is the abs value of the result + # there is overflow when |result| > max + (negative ? 1 : 0) + tmp = codegen_binary_op_with_overflow("*", u1, t2, abs, p2) + _, max = t1.range + max_result = builder.add(int(max, t1), builder.zext(negative, llvm_type(t1))) + overflow = codegen_binary_op_gt(u1, u1, tmp, max_result) + codegen_raise_overflow_cond overflow + + # negate back the result if p1 was negative + minus_tmp = builder.sub int(0, t1), tmp + builder.select negative, minus_tmp, tmp + end + def codegen_binary_extend_int(t1, t2, p1, p2) if t1.normal_rank == t2.normal_rank # Nothing to do @@ -176,138 +252,6 @@ class Crystal::CodeGenVisitor end end - def codegen_binary_op_add(t : IntegerType, t1, t2, p1, p2) - llvm_fun = case t.kind - when :i8 - binary_overflow_fun "llvm.sadd.with.overflow.i8", llvm_context.int8 - when :i16 - binary_overflow_fun "llvm.sadd.with.overflow.i16", llvm_context.int16 - when :i32 - binary_overflow_fun "llvm.sadd.with.overflow.i32", llvm_context.int32 - when :i64 - binary_overflow_fun "llvm.sadd.with.overflow.i64", llvm_context.int64 - when :i128 - binary_overflow_fun "llvm.sadd.with.overflow.i128", llvm_context.int128 - when :u8 - binary_overflow_fun "llvm.uadd.with.overflow.i8", llvm_context.int8 - when :u16 - binary_overflow_fun "llvm.uadd.with.overflow.i16", llvm_context.int16 - when :u32 - binary_overflow_fun "llvm.uadd.with.overflow.i32", llvm_context.int32 - when :u64 - binary_overflow_fun "llvm.uadd.with.overflow.i64", llvm_context.int64 - when :u128 - binary_overflow_fun "llvm.uadd.with.overflow.i128", llvm_context.int128 - else - raise "unreachable" - end - - codegen_binary_overflow_check(llvm_fun, t, t1, t2, p1, p2) - end - - def codegen_binary_op_sub(t : IntegerType, t1, t2, p1, p2) - llvm_fun = case t.kind - when :i8 - binary_overflow_fun "llvm.ssub.with.overflow.i8", llvm_context.int8 - when :i16 - binary_overflow_fun "llvm.ssub.with.overflow.i16", llvm_context.int16 - when :i32 - binary_overflow_fun "llvm.ssub.with.overflow.i32", llvm_context.int32 - when :i64 - binary_overflow_fun "llvm.ssub.with.overflow.i64", llvm_context.int64 - when :i128 - binary_overflow_fun "llvm.ssub.with.overflow.i128", llvm_context.int128 - when :u8 - binary_overflow_fun "llvm.usub.with.overflow.i8", llvm_context.int8 - when :u16 - binary_overflow_fun "llvm.usub.with.overflow.i16", llvm_context.int16 - when :u32 - binary_overflow_fun "llvm.usub.with.overflow.i32", llvm_context.int32 - when :u64 - binary_overflow_fun "llvm.usub.with.overflow.i64", llvm_context.int64 - when :u128 - binary_overflow_fun "llvm.usub.with.overflow.i128", llvm_context.int128 - else - raise "unreachable" - end - - codegen_binary_overflow_check(llvm_fun, t, t1, t2, p1, p2) - end - - def codegen_binary_op_mul(t : IntegerType, t1, t2, p1, p2) - llvm_fun = case t.kind - when :i8 - binary_overflow_fun "llvm.smul.with.overflow.i8", llvm_context.int8 - when :i16 - binary_overflow_fun "llvm.smul.with.overflow.i16", llvm_context.int16 - when :i32 - binary_overflow_fun "llvm.smul.with.overflow.i32", llvm_context.int32 - when :i64 - binary_overflow_fun "llvm.smul.with.overflow.i64", llvm_context.int64 - when :i128 - binary_overflow_fun "llvm.smul.with.overflow.i128", llvm_context.int128 - when :u8 - binary_overflow_fun "llvm.umul.with.overflow.i8", llvm_context.int8 - when :u16 - binary_overflow_fun "llvm.umul.with.overflow.i16", llvm_context.int16 - when :u32 - binary_overflow_fun "llvm.umul.with.overflow.i32", llvm_context.int32 - when :u64 - binary_overflow_fun "llvm.umul.with.overflow.i64", llvm_context.int64 - when :u128 - binary_overflow_fun "llvm.umul.with.overflow.i128", llvm_context.int128 - else - raise "unreachable" - end - - codegen_binary_overflow_check(llvm_fun, t, t1, t2, p1, p2) - end - - # Generates a call to llvm_fun(p1, p2). - # t1, t2 are the original types of p1, p2. - # t is the super type of t1 and t2 where the operation is performed. - # llvm_fun returns {res, o_bit} where the o_bit signals overflow. - # The generated code also performs a range check and truncation of res - # in order to fit in the original type t1 if needed. - # - # ``` - # %res_with_overflow = call {T, i1} (T %p1, T %p2) - # %res = extractvalue {T, i1} %res, 0 - # %o_bit = extractvalue {T, i1} %res, 1 - # ;; if T != T1 - # %out_of_range = %res < T1::MIN || %res > T1::MAX ;; compare T1.range and %res - # br i1 or(%o_bit, %out_of_range), label %overflow, label %normal - # ;; else - # br i1 %o_bit, label %overflow, label %normal - # ;; end - # - # overflow: - # ;; codegen: raise OverflowError.new with caller's location - # - # normal: - # ;; if T != T1 - # ;; %res' is returned - # %res' = trunc T %res to T1 - # ;; else - # ;; %res is returned - # ;; end - # ``` - private def codegen_binary_overflow_check(llvm_fun, t : IntegerType, t1, t2, p1, p2) - res_with_overflow = builder.call(llvm_fun, [p1, p2]) - - res = extract_value res_with_overflow, 0 - o_bit = extract_value res_with_overflow, 1 - - if t != t1 - overflow = or(o_bit, codegen_out_of_range(t1, t, res)) - else - overflow = o_bit - end - - codegen_raise_overflow_cond overflow - codegen_trunc_binary_op_result(t1, t2, res) - end - private def codegen_out_of_range(target_type : IntegerType, arg_type : IntegerType, arg) min_value, max_value = target_type.range # arg < min_value || arg > max_value @@ -792,7 +736,7 @@ class Crystal::CodeGenVisitor def codegen_primitive_pointer_set(node, target_def, call_args) type = context.type.remove_typedef.as(PointerInstanceType) - # Assinging to a Pointer(Void) has no effect + # Assigning to a Pointer(Void) has no effect return llvm_nil if type.element_type.void? value = call_args[1] @@ -860,12 +804,10 @@ class Crystal::CodeGenVisitor scope = context.type.as(NonGenericClassType) field_type = scope.instance_vars[var_name].type - # Check nil to pointer + # Check assigning nil to a field of type pointer or Proc if node.type.nil_type? && (field_type.pointer? || field_type.proc?) call_arg = llvm_c_type(field_type).null - end - - if field_type.proc? + elsif field_type.proc? call_arg = check_proc_is_not_closure(call_arg, field_type) end diff --git a/src/compiler/crystal/codegen/target.cr b/src/compiler/crystal/codegen/target.cr index 4e469fef9d4a..e6db9532c24f 100644 --- a/src/compiler/crystal/codegen/target.cr +++ b/src/compiler/crystal/codegen/target.cr @@ -55,6 +55,8 @@ class Crystal::Codegen::Target "dragonfly" when .openbsd? "openbsd" + when .netbsd? + "netbsd" else environment end @@ -84,12 +86,16 @@ class Crystal::Codegen::Target @environment.starts_with?("openbsd") end + def netbsd? + @environment.starts_with?("netbsd") + end + def linux? @environment.starts_with?("linux") end def bsd? - freebsd? || openbsd? || dragonfly? + freebsd? || netbsd? || openbsd? || dragonfly? end def unix? @@ -143,7 +149,14 @@ class Crystal::Codegen::Target opt_level = release ? LLVM::CodeGenOptLevel::Aggressive : LLVM::CodeGenOptLevel::None target = LLVM::Target.from_triple(self.to_s) - target.create_target_machine(self.to_s, cpu: cpu, features: features, opt_level: opt_level, code_model: code_model).not_nil! + machine = target.create_target_machine(self.to_s, cpu: cpu, features: features, opt_level: opt_level, code_model: code_model).not_nil! + # We need to disable global isel until https://reviews.llvm.org/D80898 is released, + # or we fixed generating values for 0 sized types. + # When removing this, also remove it from the ABI specs and jit compiler. + # See https://github.com/crystal-lang/crystal/issues/9297#issuecomment-636512270 + # for background info + machine.enable_global_isel = false + machine end def to_s(io : IO) : Nil diff --git a/src/compiler/crystal/codegen/types.cr b/src/compiler/crystal/codegen/types.cr index f156e568cd0e..5db91ee3046e 100644 --- a/src/compiler/crystal/codegen/types.cr +++ b/src/compiler/crystal/codegen/types.cr @@ -167,6 +167,15 @@ module Crystal class Const property initializer : LLVM::Value? + # Was this constant already read during the codegen phase? + # If not, and we are at the place that declares the constant, we can + # directly initialize the constant now, without checking for an `init` flag. + property? read = false + + # If true, there's no need to check whether the constant was initialized or + # not when reading it. + property? no_init_flag = false + def initialized_llvm_name "#{llvm_name}:init" end diff --git a/src/compiler/crystal/codegen/warnings.cr b/src/compiler/crystal/codegen/warnings.cr deleted file mode 100644 index 5ddee37c273a..000000000000 --- a/src/compiler/crystal/codegen/warnings.cr +++ /dev/null @@ -1,109 +0,0 @@ -module Crystal - class Program - def ignore_warning_due_to_location?(location : Location?) - return false unless location - - filename = location.original_filename - return false unless filename - - @program.warnings_exclude.any? do |path| - filename.starts_with?(path) - end - end - end - - struct DeprecatedAnnotation - getter message : String? - - def initialize(@message = nil) - end - - def self.from(ann : Annotation) - args = ann.args - named_args = ann.named_args - - if named_args - ann.raise "too many named arguments (given #{named_args.size}, expected maximum 0)" - end - - message = nil - count = 0 - - args.each do |arg| - case count - when 0 - arg.raise "first argument must be a String" unless arg.is_a?(StringLiteral) - message = arg.value - else - ann.wrong_number_of "deprecated annotation arguments", args.size, "1" - end - - count += 1 - end - - new(message) - end - end - - class Def - def short_reference - case owner - when Program - "top-level #{name}" - when .metaclass? - "#{owner.instance_type}.#{name}" - else - "#{owner}##{name}" - end - end - end - - class CodeGenVisitor - @deprecated_methods_detected = Set(String).new - - def check_call_to_deprecated_method(node : Call) - return unless @program.warnings.all? - - if (ann = node.target_def.annotation(@program.deprecated_annotation)) && - (deprecated_annotation = DeprecatedAnnotation.from(ann)) - return if compiler_expanded_call(node) - return if @program.ignore_warning_due_to_location?(node.location) - short_reference = node.target_def.short_reference - warning_key = node.location.try { |l| "#{short_reference} #{l}" } - - # skip warning if the call site was already informed - # if there is no location information just inform it. - return if !warning_key || @deprecated_methods_detected.includes?(warning_key) - @deprecated_methods_detected.add(warning_key) if warning_key - - message = deprecated_annotation.message - message = message ? " #{message}" : "" - - full_message = node.warning "Deprecated #{short_reference}.#{message}" - - @program.warning_failures << full_message - end - end - - private def compiler_expanded_call(node : Call) - # Compiler generates a `_.initialize` call in `new` - node.obj.as?(Var).try { |v| v.name == "_" } && node.name == "initialize" - end - end - - class Command - def report_warnings(result : Compiler::Result) - return if result.program.warning_failures.empty? - - result.program.warning_failures.each do |message| - STDERR.puts message - STDERR.puts "\n" - end - STDERR.puts "A total of #{result.program.warning_failures.size} warnings were found." - end - - def warnings_fail_on_exit?(result : Compiler::Result) - result.program.error_on_warnings && result.program.warning_failures.size > 0 - end - end -end diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index 497682293486..704fa80e33bc 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -50,6 +50,7 @@ class Crystal::Command end private getter options + @compiler : Compiler? def initialize(@options : Array(String)) @color = ENV["TERM"]? != "dumb" @@ -69,14 +70,14 @@ class Crystal::Command when "build".starts_with?(command) options.shift use_crystal_opts - result = build - report_warnings result - exit 1 if warnings_fail_on_exit?(result) - result + build + report_warnings + exit 1 if warnings_fail_on_exit? when "play".starts_with?(command) options.shift {% if flag?(:without_playground) %} puts "Crystal was compiled without playground support" + puts "Try the online code evaluation and sharing tool at https://play.crystal-lang.org" exit 1 {% else %} playground @@ -122,8 +123,12 @@ class Crystal::Command end end rescue ex : Crystal::LocationlessException + report_warnings + error ex.message rescue ex : Crystal::Exception + report_warnings + ex.color = @color ex.error_trace = @error_trace if @config.try(&.output_format) == "json" @@ -135,6 +140,8 @@ class Crystal::Command rescue ex : OptionParser::Exception error ex.message rescue ex + report_warnings + ex.inspect_with_backtrace STDERR error "you've found a bug in the Crystal compiler. Please open an issue, including source code that will allow us to reproduce the bug: https://github.com/crystal-lang/crystal/issues" end @@ -190,9 +197,9 @@ class Crystal::Command private def run_command(single_file = false) config = create_compiler "run", run: true, single_file: single_file if config.specified_output - result = config.compile - report_warnings result - exit 1 if warnings_fail_on_exit?(result) + config.compile + report_warnings + exit 1 if warnings_fail_on_exit? return end @@ -201,8 +208,8 @@ class Crystal::Command result = config.compile output_filename unless config.compiler.no_codegen? - report_warnings result - exit 1 if warnings_fail_on_exit?(result) + report_warnings + exit 1 if warnings_fail_on_exit? execute output_filename, config.arguments, config.compiler end @@ -297,7 +304,7 @@ class Crystal::Command private def create_compiler(command, no_codegen = false, run = false, hierarchy = false, cursor_command = false, single_file = false) - compiler = Compiler.new + compiler = new_compiler compiler.progress_tracker = @progress_tracker link_flags = [] of String filenames = [] of String @@ -462,6 +469,13 @@ class Crystal::Command filenames << stdin_filename end + if single_file + opts.before_each do |arg| + opts.stop if !arg.starts_with?('-') && arg.ends_with?(".cr") + opts.stop if File.file?(arg) + end + end + opts.unknown_args do |before, after| opt_filenames = before opt_arguments = after @@ -607,6 +621,12 @@ class Crystal::Command end private def use_crystal_opts - @options = ENV.fetch("CRYSTAL_OPTS", "").split.concat(options) + @options = Process.parse_arguments(ENV.fetch("CRYSTAL_OPTS", "")).concat(options) + rescue ex + raise LocationlessException.new("Failed to parse CRYSTAL_OPTS: #{ex.message}") + end + + private def new_compiler + @compiler = Compiler.new end end diff --git a/src/compiler/crystal/command/docs.cr b/src/compiler/crystal/command/docs.cr index 24dc922e7a13..573bb3a4fc63 100644 --- a/src/compiler/crystal/command/docs.cr +++ b/src/compiler/crystal/command/docs.cr @@ -14,7 +14,7 @@ class Crystal::Command sitemap_changefreq = "never" project_info = Doc::ProjectInfo.new - compiler = Compiler.new + compiler = new_compiler OptionParser.parse(options) do |opts| opts.banner = <<-'BANNER' @@ -33,6 +33,14 @@ class Crystal::Command project_info.version = value end + opts.on("--source-refname=REFNAME", "Set source refname (e.g. git tag, commit hash)") do |value| + project_info.refname = value + end + + opts.on("--source-url-pattern=REFNAME", "Set URL pattern for source code links") do |value| + project_info.source_url_pattern = value + end + opts.on("--output=DIR", "-o DIR", "Set the output directory (default: #{output_directory})") do |value| output_directory = value end @@ -44,7 +52,7 @@ class Crystal::Command output_format = value end - opts.on("--json_config_url=URL", "Set the URL pointing to a config file (used for discovering versions)") do |value| + opts.on("--json-config-url=URL", "Set the URL pointing to a config file (used for discovering versions)") do |value| project_info.json_config_url = value end @@ -132,7 +140,7 @@ class Crystal::Command Doc::Generator.new(result.program, included_dirs, output_directory, output_format, sitemap_base_url, sitemap_priority, sitemap_changefreq, project_info).run - report_warnings result - exit 1 if warnings_fail_on_exit?(result) + report_warnings + exit 1 if warnings_fail_on_exit? end end diff --git a/src/compiler/crystal/command/env.cr b/src/compiler/crystal/command/env.cr index 5b6c20bc1999..5e41e0fb0365 100644 --- a/src/compiler/crystal/command/env.cr +++ b/src/compiler/crystal/command/env.cr @@ -16,7 +16,7 @@ class Crystal::Command if ARGV.empty? vars.each do |key, value| - puts "#{key}=#{value.inspect}" + puts "#{key}=#{Process.quote(value)}" end else ARGV.each do |key| diff --git a/src/compiler/crystal/command/eval.cr b/src/compiler/crystal/command/eval.cr index f982d19f6efb..efed166c358c 100644 --- a/src/compiler/crystal/command/eval.cr +++ b/src/compiler/crystal/command/eval.cr @@ -2,7 +2,7 @@ class Crystal::Command private def eval - compiler = Compiler.new + compiler = new_compiler OptionParser.parse(options) do |opts| opts.banner = "Usage: crystal eval [options] [source]\n\nOptions:" setup_simple_compiler_options compiler, opts diff --git a/src/compiler/crystal/command/spec.cr b/src/compiler/crystal/command/spec.cr index 3a1137450478..0af8808a205a 100644 --- a/src/compiler/crystal/command/spec.cr +++ b/src/compiler/crystal/command/spec.cr @@ -10,7 +10,7 @@ class Crystal::Command private def spec - compiler = Compiler.new + compiler = new_compiler OptionParser.parse(options) do |opts| opts.banner = "Usage: crystal spec [options] [files]\n\nOptions:" setup_simple_compiler_options compiler, opts @@ -66,13 +66,15 @@ class Crystal::Command source_filename = File.expand_path("spec") - source = target_filenames.map { |filename| %(require "./#{filename}") }.join('\n') + source = target_filenames.map { |filename| + %(require "./#{::Path[filename].to_posix}") + }.join('\n') sources = [Compiler::Source.new(source_filename, source)] output_filename = Crystal.temp_executable "spec" result = compiler.compile sources, output_filename - report_warnings result - execute output_filename, options, compiler, error_on_exit: warnings_fail_on_exit?(result) + report_warnings + execute output_filename, options, compiler, error_on_exit: warnings_fail_on_exit? end end diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index febd73636a7c..305e0a1122e6 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -95,7 +95,7 @@ module Crystal # Codegen target to use in the compilation. # If not set, asks LLVM the default one for the current machine. - property codegen_target = Config.default_target + property codegen_target = Config.host_target # If `true`, prints the link command line that is performed # to create the executable. @@ -153,6 +153,9 @@ module Crystal # Whether to use llvm ThinLTO for linking property thin_lto = false + # Program that was created for the last compilation. + property! program : Program + # Compiles the given *source*, with *output_filename* as the name # of the generated executable. # @@ -198,7 +201,7 @@ module Crystal end private def new_program(sources) - program = Program.new + @program = program = Program.new program.filename = sources.first.filename program.cache_dir = CacheDir.instance.directory_for(sources) program.codegen_target = codegen_target @@ -274,7 +277,7 @@ module Crystal units = llvm_modules.map do |type_name, info| llvm_mod = info.mod llvm_mod.target = target_triple - CompilationUnit.new(self, type_name, llvm_mod, output_dir, bc_flags_changed) + CompilationUnit.new(self, program, type_name, llvm_mod, output_dir, bc_flags_changed) end if @cross_compile @@ -304,7 +307,7 @@ module Crystal private def cross_compile(program, units, output_filename) unit = units.first llvm_mod = unit.llvm_mod - object_name = "#{output_filename}.o" + object_name = output_filename + program.object_extension optimize llvm_mod if @release @@ -318,7 +321,7 @@ module Crystal end private def print_command(command, args) - stdout.puts command.sub(%("${@}"), args && args.join(" ")) + stdout.puts command.sub(%("${@}"), args && Process.quote(args)) end private def linker_command(program : Program, object_names, output_filename, output_dir, expand = false) @@ -327,7 +330,10 @@ module Crystal # Execute and expand `subcommands`. lib_flags = lib_flags.gsub(/`(.*?)`/) { `#{$1}` } if expand - args = %(#{object_names.join(" ")} "/Fe#{output_filename}" #{lib_flags} #{@link_flags}) + object_arg = Process.quote_windows(object_names) + output_arg = Process.quote_windows("/Fe#{output_filename}") + + args = %(/nologo #{object_arg} #{output_arg} #{lib_flags} #{@link_flags}) cmd = "#{CL} #{args}" if cmd.to_utf16.size > 32000 @@ -339,7 +345,7 @@ module Crystal args_filename = "#{output_dir}/linker_args.txt" File.write(args_filename, args_bytes) - cmd = "#{CL} @#{args_filename}" + cmd = "#{CL} #{Process.quote_windows("@" + args_filename)}" end {cmd, nil} @@ -359,9 +365,8 @@ module Crystal link_flags = @link_flags || "" link_flags += " -rdynamic" - link_flags += " -static" if static? - { %(#{cc} "${@}" -o '#{output_filename}' #{link_flags} #{program.lib_flags}), object_names } + { %(#{cc} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names } end end @@ -597,9 +602,10 @@ module Crystal getter original_name getter llvm_mod getter? reused_previous_compilation = false + @object_extension : String - def initialize(@compiler : Compiler, @name : String, @llvm_mod : LLVM::Module, - @output_dir : String, @bc_flags_changed : Bool) + def initialize(@compiler : Compiler, program : Program, @name : String, + @llvm_mod : LLVM::Module, @output_dir : String, @bc_flags_changed : Bool) @name = "_main" if @name == "" @original_name = @name @name = String.build do |str| @@ -621,6 +627,8 @@ module Crystal # 17 chars from name + 1 (dash) + 32 (md5) = 50 @name = "#{@name[0..16]}-#{Digest::MD5.hexdigest(@name)}" end + + @object_extension = program.object_extension end def compile @@ -675,7 +683,7 @@ module Crystal if can_reuse_previous_compilation memory_io = IO::Memory.new(memory_buffer.to_slice) - changed = File.open(bc_name) { |bc_file| !FileUtils.cmp(bc_file, memory_io) } + changed = File.open(bc_name) { |bc_file| !IO.same_content?(bc_file, memory_io) } # If the user cancelled a previous compilation # it might be that the .o file is empty @@ -690,6 +698,8 @@ module Crystal # If there's a memory buffer, it means we must create a .o from it if memory_buffer + # Delete existing .o file. It cannot be used anymore. + File.delete(object_name) if File.exists?(object_name) # Create the .bc file (for next compilations) File.write(bc_name, memory_buffer.to_slice) memory_buffer.dispose @@ -721,7 +731,7 @@ module Crystal llvm_mod.print_to_file "#{output_filename}.ll" end if emit_target.obj? - FileUtils.cp(object_name, "#{output_filename}.o") + FileUtils.cp(object_name, output_filename + @object_extension) end end @@ -730,7 +740,7 @@ module Crystal end def object_filename - "#{@name}.o" + @name + @object_extension end def temporary_object_name diff --git a/src/compiler/crystal/config.cr b/src/compiler/crystal/config.cr index 2cfa54023836..eb77b9e8886f 100644 --- a/src/compiler/crystal/config.cr +++ b/src/compiler/crystal/config.cr @@ -20,7 +20,7 @@ module Crystal Crystal #{version} #{formatted_sha}(#{date}) LLVM: #{llvm_version} - Default target: #{self.default_target} + Default target: #{self.host_target} DOC end @@ -32,14 +32,18 @@ module Crystal end def self.date - time = {{ (env("SOURCE_DATE_EPOCH") || `date +%s`).to_i }} - Time.unix(time).to_s("%Y-%m-%d") + source_date_epoch = {{ (t = env("SOURCE_DATE_EPOCH")) && !t.empty? ? t.to_i : nil }} + if source_date_epoch + Time.unix(source_date_epoch).to_s("%Y-%m-%d") + else + "" + end end - @@default_target : Crystal::Codegen::Target? + @@host_target : Crystal::Codegen::Target? - def self.default_target : Crystal::Codegen::Target - @@default_target ||= begin + def self.host_target : Crystal::Codegen::Target + @@host_target ||= begin target = Crystal::Codegen::Target.new({{env("CRYSTAL_CONFIG_TARGET")}} || LLVM.default_target_triple) if target.linux? diff --git a/src/compiler/crystal/crystal_path.cr b/src/compiler/crystal/crystal_path.cr index 16c79c3a8559..9b2bdd982df7 100644 --- a/src/compiler/crystal/crystal_path.cr +++ b/src/compiler/crystal/crystal_path.cr @@ -3,27 +3,42 @@ require "./exception" module Crystal struct CrystalPath - class Error < LocationlessException + class NotFoundError < LocationlessException + getter filename + getter relative_to + + def initialize(@filename : String, @relative_to : String?) + end end + private DEFAULT_LIB_PATH = "lib" + def self.default_path - ENV["CRYSTAL_PATH"]? || Crystal::Config.path + ENV["CRYSTAL_PATH"]? || begin + if Crystal::Config.path.blank? + DEFAULT_LIB_PATH + elsif Crystal::Config.path.split(Process::PATH_DELIMITER).includes?(DEFAULT_LIB_PATH) + Crystal::Config.path + else + {DEFAULT_LIB_PATH, Crystal::Config.path}.join(Process::PATH_DELIMITER) + end + end end - @crystal_path : Array(String) + property entries : Array(String) - def initialize(path = CrystalPath.default_path, codegen_target = Config.default_target) - @crystal_path = path.split(Process::PATH_DELIMITER).reject &.empty? + def initialize(path = CrystalPath.default_path, codegen_target = Config.host_target) + @entries = path.split(Process::PATH_DELIMITER).reject &.empty? add_target_path(codegen_target) end private def add_target_path(codegen_target) target = "#{codegen_target.architecture}-#{codegen_target.os_name}" - @crystal_path.each do |path| + @entries.each do |path| path = File.join(path, "lib_c", target) if Dir.exists?(path) - @crystal_path << path unless @crystal_path.includes?(path) + @entries << path unless @entries.includes?(path) return end end @@ -38,7 +53,9 @@ module Crystal result = find_in_crystal_path(filename) end - cant_find_file filename, relative_to unless result + unless result + raise NotFoundError.new(filename, relative_to) + end result = [result] if result.is_a?(String) result @@ -144,30 +161,12 @@ module Crystal end private def find_in_crystal_path(filename) - @crystal_path.each do |path| + @entries.each do |path| required = find_in_path_relative_to_dir(filename, path) return required if required end nil end - - private def cant_find_file(filename, relative_to) - error = "can't find file '#{filename}'" - - if filename.starts_with? '.' - error += " relative to '#{relative_to}'" if relative_to - else - error = <<-NOTE - #{error} - - If you're trying to require a shard: - - Did you remember to run `shards install`? - - Did you make sure you're running the compiler in the same directory as your shard.yml? - NOTE - end - - raise Error.new(error) - end end end diff --git a/src/compiler/crystal/exception.cr b/src/compiler/crystal/exception.cr index 5f3bb580d52a..0394d600f991 100644 --- a/src/compiler/crystal/exception.cr +++ b/src/compiler/crystal/exception.cr @@ -9,14 +9,14 @@ module Crystal @filename : String | VirtualFile | Nil def to_s(io) : Nil - to_s_with_source(nil, io) + to_s_with_source(io, nil) end def warning=(warning) @warning = !!warning end - abstract def to_s_with_source(source, io) + abstract def to_s_with_source(io : IO, source) def to_json(json : JSON::Builder) json.array do @@ -43,7 +43,7 @@ module Crystal def to_s_with_source(source) String.build do |io| - to_s_with_source source, io + to_s_with_source(io, source) end end @@ -77,11 +77,11 @@ module Crystal end class LocationlessException < Exception - def to_s_with_source(source, io) + def to_s_with_source(io : IO, source) io << @message end - def append_to_s(source, io) + def append_to_s(io : IO, source) io << @message end diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index 174f43f9724f..eaa57510e2a4 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -36,6 +36,16 @@ module Crystal::Macros def flag?(name) : BoolLiteral end + # Returns whether a [compile-time flag](https://crystal-lang.org/docs/syntax_and_semantics/compile_time_flags.html) + # is set for the *host* platform, which can differ from the target platform + # (`flag?`) during cross-compilation. + # + # ``` + # {{ host_flag?(:win32) }} # true or false + # ``` + def host_flag?(name) : BoolLiteral + end + # Prints AST nodes at compile-time. Useful for debugging macros. def puts(*expressions) : Nop end @@ -456,6 +466,10 @@ module Crystal::Macros def strip : StringLiteral end + # Similar to `String#titleize`. + def titleize : StringLiteral + end + # Similar to `String#to_i`. def to_i(base = 10) end @@ -557,6 +571,10 @@ module Crystal::Macros def strip : SymbolLiteral end + # Similar to `String#titleize`. + def titleize : SymbolLiteral + end + # Similar to `String#tr`. def tr(from : StringLiteral, to : StringLiteral) : SymbolLiteral end @@ -1597,7 +1615,7 @@ module Crystal::Macros # class MagicConstant < ASTNode # end - # A fictitious node representing an idenfitifer like, `foo`, `Bar` or `something_else`. + # A fictitious node representing an identifier like, `foo`, `Bar` or `something_else`. # # The parser doesn't create this nodes. Instead, you create them by invoking `id` # on some nodes. For example, invoking `id` on a `StringLiteral` returns a `MacroId` @@ -1717,6 +1735,10 @@ module Crystal::Macros def strip : MacroId end + # Similar to `String#titleize`. + def titleize : MacroId + end + # Similar to `String#tr`. def tr(from : StringLiteral, to : StringLiteral) : MacroId end @@ -1728,24 +1750,99 @@ module Crystal::Macros # Represents a type in the program, like `Int32` or `String`. class TypeNode < ASTNode - # Returns `true` if this type is abstract. + # Returns `true` if `self` is abstract, otherwise `false`. + # + # ``` + # module One; end + # + # abstract struct Two; end + # + # class Three; end + # + # abstract class Four; end + # + # {{One.abstract?}} # => false + # {{Two.abstract?}} # => true + # {{Three.abstract?}} # => false + # {{Four.abstract?}} # => true + # ``` def abstract? : BoolLiteral end - # Returns `true` if this type is a union type, `false` otherwise. + # Returns `true` if `self` is a union type, otherwise `false`. # - # See also: `union_types`. + # See also: `#union_types`. + # + # ``` + # {{String.union?}} # => false + # {{String?.union?}} # => true + # {{Union(String, Bool).union?}} # => true + # ``` def union? : BoolLiteral end - # Returns `true` if this type is nilable (if it has `Nil` amongst its types). + # Returns `true` if `self` is nilable (if it has `Nil` amongst its types), otherwise `false`. + # + # ``` + # {{String.nilable?}} # => false + # {{String?.nilable?}} # => true + # {{Union(String, Bool, Nil).nilable?}} # => true + # ``` def nilable? : BoolLiteral end + # Returns `true` if `self` is a `module`, otherwise `false`. + # + # ``` + # module One; end + # + # class Two; end + # + # struct Three; end + # + # {{One.module?}} # => true + # {{Two.module?}} # => false + # {{Three.module?}} # => false + # ``` + def module? : BoolLiteral + end + + # Returns `true` if `self` is a `class`, otherwise `false`. + # + # ``` + # module One; end + # + # class Two; end + # + # struct Three; end + # + # {{One.class?}} # => false + # {{Two.class?}} # => true + # {{Three.class?}} # => false + # ``` + def class? : BoolLiteral + end + + # Returns `true` if `self` is a `struct`, otherwise `false`. + # + # ``` + # module One; end + # + # class Two; end + # + # struct Three; end + # + # {{One.struct?}} # => false + # {{Two.struct?}} # => false + # {{Three.struct?}} # => true + # ``` + def struct? : BoolLiteral + end + # Returns the types forming a union type, if this is a union type. # Otherwise returns this single type inside an array literal (so you can safely call `union_types` on any type and treat all types uniformly). # - # See also: `union?`. + # See also: `#union?`. def union_types : ArrayLiteral(TypeNode) end diff --git a/src/compiler/crystal/macros/interpreter.cr b/src/compiler/crystal/macros/interpreter.cr index 1c804a301f36..db5bcceff172 100644 --- a/src/compiler/crystal/macros/interpreter.cr +++ b/src/compiler/crystal/macros/interpreter.cr @@ -318,6 +318,11 @@ module Crystal false end + def visit(node : OpAssign) + @program.normalize(node).accept(self) + false + end + def visit(node : And) node.left.accept self if @last.truthy? diff --git a/src/compiler/crystal/macros/macros.cr b/src/compiler/crystal/macros/macros.cr index bd08725be600..f16460c5b86f 100644 --- a/src/compiler/crystal/macros/macros.cr +++ b/src/compiler/crystal/macros/macros.cr @@ -16,6 +16,8 @@ class Crystal::Program property compiled_macros_cache = {} of String => CompiledMacroRun def expand_macro(a_macro : Macro, call : Call, scope : Type, path_lookup : Type? = nil, a_def : Def? = nil) + check_call_to_deprecated_macro a_macro, call + interpreter = MacroInterpreter.new self, scope, path_lookup || scope, a_macro, call, a_def, in_macro: true a_macro.body.accept interpreter {interpreter.to_s, interpreter.macro_expansion_pragmas} @@ -158,7 +160,7 @@ class Crystal::Program begin files = @program.find_in_path(recorded_require.filename, recorded_require.relative_to) required_files.concat(files) if files - rescue Crystal::CrystalPath::Error + rescue Crystal::CrystalPath::NotFoundError # Maybe the file is gone next end diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index 45dc8936c382..4bf8f5e91560 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -51,7 +51,7 @@ module Crystal interpret_debug(node) when "env" interpret_env(node) - when "flag?" + when "flag?", "host_flag?" interpret_flag?(node) when "puts" interpret_puts(node) @@ -145,10 +145,18 @@ module Crystal def interpret_flag?(node) if node.args.size == 1 node.args[0].accept self - flag = @last.to_macro_id - @last = BoolLiteral.new(@program.has_flag?(flag)) + flag_name = @last.to_macro_id + flags = case node.name + when "flag?" + @program.flags + when "host_flag?" + @program.host_flags + else + raise "Bug: unexpected macro method #{node.name}" + end + @last = BoolLiteral.new(flags.includes?(flag_name)) else - node.wrong_number_of_arguments "macro call 'flag?'", node.args.size, 1 + node.wrong_number_of_arguments "macro call '#{node.name}'", node.args.size, 1 end end @@ -257,7 +265,7 @@ module Crystal if result.status.success? @last = MacroId.new(result.stdout) else - command = "#{original_filename} #{run_args.map(&.inspect).join " "}" + command = "#{Process.quote(original_filename)} #{Process.quote(run_args)}" message = IO::Memory.new message << "Error executing run (exit code: #{result.status.exit_code}): #{command}\n" @@ -344,22 +352,22 @@ module Crystal end when "line_number" interpret_argless_method("line_number", args) do - line_number = location.try &.original_location.try &.line_number + line_number = location.try &.expanded_location.try &.line_number line_number ? NumberLiteral.new(line_number) : NilLiteral.new end when "column_number" interpret_argless_method("column_number", args) do - column_number = location.try &.original_location.try &.column_number + column_number = location.try &.expanded_location.try &.column_number column_number ? NumberLiteral.new(column_number) : NilLiteral.new end when "end_line_number" interpret_argless_method("end_line_number", args) do - line_number = end_location.try &.original_location.try &.line_number + line_number = end_location.try &.expanded_location.try &.line_number line_number ? NumberLiteral.new(line_number) : NilLiteral.new end when "end_column_number" interpret_argless_method("end_column_number", args) do - column_number = end_location.try &.original_location.try &.column_number + column_number = end_location.try &.expanded_location.try &.column_number column_number ? NumberLiteral.new(column_number) : NilLiteral.new end when "==" @@ -752,6 +760,8 @@ module Crystal end when "strip" interpret_argless_method(method, args) { StringLiteral.new(@value.strip) } + when "titleize" + interpret_argless_method(method, args) { StringLiteral.new(@value.titleize) } when "to_i" case args.size when 0 @@ -1555,6 +1565,12 @@ module Crystal interpret_argless_method(method, args) { BoolLiteral.new(type.abstract?) } when "union?" interpret_argless_method(method, args) { BoolLiteral.new(type.is_a?(UnionType)) } + when "module?" + interpret_argless_method(method, args) { BoolLiteral.new(type.module?) } + when "class?" + interpret_argless_method(method, args) { BoolLiteral.new(type.class? && !type.struct?) } + when "struct?" + interpret_argless_method(method, args) { BoolLiteral.new(type.class? && type.struct?) } when "nilable?" interpret_argless_method(method, args) { BoolLiteral.new(type.nilable?) } when "union_types" @@ -1733,7 +1749,7 @@ module Crystal end def self.type_vars(type) - if type.is_a?(GenericClassInstanceType) + if type.is_a?(GenericClassInstanceType) || type.is_a?(GenericModuleInstanceType) if type.is_a?(TupleInstanceType) if type.tuple_types.empty? empty_no_return_array diff --git a/src/compiler/crystal/program.cr b/src/compiler/crystal/program.cr index 2186fa7f9bba..1730958be238 100644 --- a/src/compiler/crystal/program.cr +++ b/src/compiler/crystal/program.cr @@ -116,19 +116,7 @@ module Crystal # A `ProgressTracker` object which tracks compilation progress. property progress_tracker = ProgressTracker.new - property codegen_target = Config.default_target - - # Which kind of warnings wants to be detected. - property warnings : Warnings = Warnings::All - - # Paths to ignore for warnings detection. - property warnings_exclude : Array(String) = [] of String - - # Detected warning failures. - property warning_failures = [] of String - - # If `true` compiler will error if warnings are found. - property error_on_warnings : Bool = false + property codegen_target = Config.host_target def initialize super(self, self, "main") @@ -242,6 +230,7 @@ module Crystal types["ReturnsTwice"] = @returns_twice_annotation = AnnotationType.new self, self, "ReturnsTwice" types["ThreadLocal"] = @thread_local_annotation = AnnotationType.new self, self, "ThreadLocal" types["Deprecated"] = @deprecated_annotation = AnnotationType.new self, self, "Deprecated" + types["Experimental"] = @experimental_annotation = AnnotationType.new self, self, "Experimental" define_crystal_constants end @@ -325,10 +314,18 @@ module Crystal # Returns the `Type` for `type | Nil` def nilable(type) - # Nil | Nil # => Nil - return self.nil if type == self.nil - - union_of self.nil, type + case type + when self.nil + # Nil | Nil # => Nil + return self.nil + when UnionType + types = Array(Type).new(type.union_types.size + 1) + types.concat type.union_types + types << self.nil unless types.includes? self.nil + union_of types + else + union_of self.nil, type + end end # Returns the `Type` for `type1 | type2` @@ -451,7 +448,7 @@ module Crystal packed_annotation thread_local_annotation no_inline_annotation always_inline_annotation naked_annotation returns_twice_annotation raises_annotation primitive_annotation call_convention_annotation - flags_annotation link_annotation extern_annotation deprecated_annotation) %} + flags_annotation link_annotation extern_annotation deprecated_annotation experimental_annotation) %} def {{name.id}} @{{name.id}}.not_nil! end @@ -485,6 +482,30 @@ module Crystal end end + def int_type(signed, size) + if signed + case size + when 1 then int8 + when 2 then int16 + when 4 then int32 + when 8 then int64 + when 16 then int128 + else + raise "BUG: Invalid int size: #{size}" + end + else + case size + when 1 then uint8 + when 2 then uint16 + when 4 then uint32 + when 8 then uint64 + when 16 then uint128 + else + raise "BUG: Invalid int size: #{size}" + end + end + end + # Returns the `IntegerType` that matches the given Int value def int?(int) case int @@ -560,8 +581,8 @@ module Crystal end end - def lookup_private_matches(filename, signature) - file_module?(filename).try &.lookup_matches(signature) + def lookup_private_matches(filename, signature, analyze_all = false) + file_module?(filename).try &.lookup_matches(signature, analyze_all: analyze_all) end def file_module?(filename) diff --git a/src/compiler/crystal/semantic.cr b/src/compiler/crystal/semantic.cr index b7ac36231e59..4e2ddef0993b 100644 --- a/src/compiler/crystal/semantic.cr +++ b/src/compiler/crystal/semantic.cr @@ -4,7 +4,7 @@ require "./syntax/visitor" require "./semantic/*" # The overall algorithm for semantic analysis of a program is: -# - top level: declare clases, modules, macros, defs and other top-level stuff +# - top level: declare classes, modules, macros, defs and other top-level stuff # - new methods: create `new` methods for every `initialize` method # - type declarations: process type declarations like `@x : Int32` # - check abstract defs: check that abstract defs are implemented @@ -70,12 +70,8 @@ class Crystal::Program TypeDeclarationProcessor.new(self).process(node) end - # TODO: remove this check a couple of versions after 0.30.0 once - # we are sure it's working fine for everyone - unless has_flag?("skip_abstract_def_check") - @progress_tracker.stage("Semantic (abstract def check)") do - AbstractDefChecker.new(self).run - end + @progress_tracker.stage("Semantic (abstract def check)") do + AbstractDefChecker.new(self).run end {node, processor} diff --git a/src/compiler/crystal/semantic/abstract_def_checker.cr b/src/compiler/crystal/semantic/abstract_def_checker.cr index 418cf388c609..e0dafd0b5bb6 100644 --- a/src/compiler/crystal/semantic/abstract_def_checker.cr +++ b/src/compiler/crystal/semantic/abstract_def_checker.cr @@ -22,8 +22,6 @@ # def foo(x); end # end # ``` -# -# TODO: the check currently ignores methods that involve splats. class Crystal::AbstractDefChecker def initialize(@program : Program) @all_checked = Set(Type).new @@ -48,9 +46,6 @@ class Crystal::AbstractDefChecker defs_with_metadata.each do |def_with_metadata| a_def = def_with_metadata.def if a_def.abstract? - # TODO: for now we skip methods with splats and default arguments - next if a_def.splat_index || a_def.args.any? &.default_value - check_implemented_in_subtypes(type, a_def) end end @@ -120,10 +115,8 @@ class Crystal::AbstractDefChecker return false unless m1.name == m2.name return false unless m1.yields == m2.yields - # TODO: for now we consider that if there's a splat, the method is implemented - return true if m1.splat_index - - return false if m1.args.size < m2.args.size + m1_args, m1_kargs = def_arg_ranges(m1) + m2_args, m2_kargs = def_arg_ranges(m2) # If the base type is a generic type, we find the generic instantiation of # t1 for it. This will have a mapping of type vars to types, for example @@ -137,25 +130,124 @@ class Crystal::AbstractDefChecker m2 = replace_method_arg_paths_with_type_vars(t2, m2, generic_base) end - m2.args.zip(m1.args) do |a2, a1| - r1 = a1.restriction - r2 = a2.restriction - if r2 && r1 && r1 != r2 - # Check if a1.restriction is contravariant with a2.restriction - begin - rt1 = t1.lookup_type(r1) - rt2 = t2.lookup_type(r2) - return false unless rt2.covariant?(rt1) - rescue Crystal::TypeException - # Ignore if we can't find a type (assume the method is implemented) - next - end + # First check positional arguments + # The following algorithm walk through the arguments in the abstract + # method and the implementation at the same time, until a splat argument is found + # or the end of the positional argument list is reached in both lists. + # The table below resumes the allowed cases (OK) and rejected (x) for each combination + # of the argument in the implementation (a1) and the abstract def (a2). + # `an = Dn` represents an argument with a default value. `-` represents that + # no more arguments are available to compare. + # Allowed cases are then verified that they have compatible default value + # and type restrictions. + # + # | a2 | a2 = D2 | *a2 | - | + # a1 | OK | x | x | x | + # a1 = D1 | OK | OK | OK | OK | + # *a1 | OK | x | OK | OK | + # - | x | x | x | OK | + i1 = i2 = 0 + loop do + a1 = i1 <= m1_args ? m1.args[i1] : nil + a2 = i2 <= m2_args ? m2.args[i2] : nil + + case + when !a1 + # No more arguments in the implementation + return false unless !a2 + when i1 == m1.splat_index + # The argument in the implementation is a splat + return false if a2 && a2.default_value + when !a1.default_value + # The argument in the implementation doesn't have a default value + return false if !a2 || a2.default_value || i2 == m2.splat_index + end + + if a1 && a2 + return false unless check_arg(t1, a1, t2, a2) + end + + # Move next, unless we're on the splat already or at the end of the arguments + done = true + unless i1 == m1.splat_index || a1 == nil + i1 += 1 + done = false + end + unless i2 == m2.splat_index || a2 == nil + i2 += 1 + done = false end + break if done end - # If the method has more arguments, but default values for them, it implements it - if m1.args.size > m2.args.size - return false unless m1.args[m2.args.size].default_value + # Index keyword arguments + kargs = + m1_kargs.to_h do |i| + a1 = m1.args[i] + {a1.name, a1} + end + + # Check double splat + if m2_double_splat = m2.double_splat + if m1_double_splat = m1.double_splat + return false unless check_arg(t1, m1_double_splat, t2, m2_double_splat) + else + return false + end + end + + # Check keyword arguments + # They must either exist in the implementation or match with the double splat + m2_kargs.each do |i| + a2 = m2.args[i] + if a1 = kargs.delete(a2.name) || m1.double_splat + return false unless check_arg(t1, a1, t2, a2) + else + return false + end + end + + # Check remaining keyword arguments + # They must have a default value and match the double splat in the abstract (if it exists) + kargs.each_value do |a1| + return false unless a1.default_value + if m2_double_splat = m2.double_splat + return false unless check_arg(t1, a1, t2, m2_double_splat) + end + end + + true + end + + private def def_arg_ranges(method : Def) + if splat = method.splat_index + if method.args[splat].name.size == 0 + {splat - 1, (splat + 1...method.args.size)} + else + {splat, (splat + 1...method.args.size)} + end + else + {method.args.size - 1, (0...0)} + end + end + + def check_arg(t1 : Type, a1 : Arg, t2 : Type, a2 : Arg) + if a2.default_value + return false unless a1.default_value == a2.default_value + end + + r1 = a1.restriction + r2 = a2.restriction + if r2 && r1 && r1 != r2 + # Check if a1.restriction is contravariant with a2.restriction + begin + rt1 = t1.lookup_type(r1) + rt2 = t2.lookup_type(r2) + return false unless rt2.covariant?(rt1) + rescue Crystal::TypeException + # Ignore if we can't find a type (assume the method is implemented) + return true + end end true @@ -169,7 +261,7 @@ class Crystal::AbstractDefChecker original_base_return_type = base_type.lookup_type?(base_return_type_node) unless original_base_return_type - report_warning(base_return_type_node, "can't resolve return type #{base_return_type_node}\n#{this_warning_will_become_an_error}") + report_error(base_return_type_node, "can't resolve return type #{base_return_type_node}") return end @@ -187,24 +279,24 @@ class Crystal::AbstractDefChecker base_return_type = base_type.lookup_type?(base_return_type_node) unless base_return_type - report_warning(base_return_type_node, "can't resolve return type #{base_return_type_node}\n#{this_warning_will_become_an_error}") + report_error(base_return_type_node, "can't resolve return type #{base_return_type_node}") return end return_type_node = method.return_type unless return_type_node - report_warning(method, "this method overrides #{Call.def_full_name(base_type, base_method)} which has an explicit return type of #{original_base_return_type}.\n#{@program.colorize("Please add an explicit return type (#{base_return_type} or a subtype of it) to this method as well.").yellow.bold}\n\n#{this_warning_will_become_an_error}") + report_error(method, "this method overrides #{Call.def_full_name(base_type, base_method)} which has an explicit return type of #{original_base_return_type}.\n#{@program.colorize("Please add an explicit return type (#{base_return_type} or a subtype of it) to this method as well.").yellow.bold}\n") return end return_type = type.lookup_type?(return_type_node) unless return_type - report_warning(return_type_node, "can't resolve return type #{return_type_node}\n#{this_warning_will_become_an_error}") + report_error(return_type_node, "can't resolve return type #{return_type_node}") return end unless return_type.implements?(base_return_type) - report_warning(return_type_node, "this method must return #{base_return_type}, which is the return type of the overridden method #{Call.def_full_name(base_type, base_method)}, or a subtype of it, not #{return_type}\n#{this_warning_will_become_an_error}") + report_error(return_type_node, "this method must return #{base_return_type}, which is the return type of the overridden method #{Call.def_full_name(base_type, base_method)}, or a subtype of it, not #{return_type}") return end end @@ -229,8 +321,8 @@ class Crystal::AbstractDefChecker @program.colorize("The above warning will become an error in a future Crystal version.").yellow.bold end - private def report_warning(node, message) - @program.report_warning(node, message) + private def report_error(node, message) + node.raise(message, nil) end class ReplacePathWithTypeVar < Visitor diff --git a/src/compiler/crystal/semantic/ast.cr b/src/compiler/crystal/semantic/ast.cr index 41819a51e2fd..80c12bb70c8d 100644 --- a/src/compiler/crystal/semantic/ast.cr +++ b/src/compiler/crystal/semantic/ast.cr @@ -18,7 +18,7 @@ module Crystal String.build do |io| exception = exception_type.for_node(self, message, inner) exception.warning = true - exception.append_to_s(nil, io) + exception.append_to_s(io, nil) end end @@ -119,6 +119,8 @@ module Crystal end class Def + include Annotatable + property! owner : Type property! original_owner : Type property vars : MetaVars? @@ -147,9 +149,6 @@ module Crystal # Is this a `new` method that was expanded from an initialize? property? new = false - # Annotations on this def - property annotations : Hash(AnnotationType, Array(Annotation))? - @macro_owner : Type? def macro_owner=(@macro_owner) @@ -179,23 +178,6 @@ module Crystal end end - # Adds an annotation with the given type and value - def add_annotation(annotation_type : AnnotationType, value : Annotation) - annotations = @annotations ||= {} of AnnotationType => Array(Annotation) - annotations[annotation_type] ||= [] of Annotation - annotations[annotation_type] << value - end - - # Returns the last defined annotation with the given type, if any, or `nil` otherwise - def annotation(annotation_type) : Annotation? - @annotations.try &.[annotation_type]?.try &.last? - end - - # Returns all annotations with the given type, if any, or `nil` otherwise - def annotations(annotation_type) : Array(Annotation)? - @annotations.try &.[annotation_type]? - end - # Returns the minimum and maximum number of arguments that must # be passed to this method. def min_max_args_sizes @@ -250,6 +232,10 @@ module Crystal end class Macro + include Annotatable + + property! owner : Type + # Yields `arg, arg_index, object, object_index` corresponding # to arguments matching the given objects, taking into account this # macro's splat index. @@ -494,6 +480,8 @@ module Crystal # A variable belonging to a type: a global, # class or instance variable (globals belong to the program). class MetaTypeVar < Var + include Annotatable + property nil_reason : NilReason? # The owner of this variable, useful for showing good @@ -514,9 +502,6 @@ module Crystal # Is this variable "unsafe" (no need to check if it was initialized)? property? uninitialized = false - # Annotations of this instance var - property annotations : Hash(AnnotationType, Array(Annotation))? - def kind case name[0] when '@' @@ -533,23 +518,6 @@ module Crystal def global? kind == :global end - - # Adds an annotation with the given type and value - def add_annotation(annotation_type : AnnotationType, value : Annotation) - annotations = @annotations ||= {} of AnnotationType => Array(Annotation) - annotations[annotation_type] ||= [] of Annotation - annotations[annotation_type] << value - end - - # Returns the last defined annotation with the given type, if any, or `nil` otherwise - def annotation(annotation_type) : Annotation? - @annotations.try &.[annotation_type]?.try &.last? - end - - # Returns all annotations with the given type, if any, or `nil` otherwise - def annotations(annotation_type) : Array(Annotation)? - @annotations.try &.[annotation_type]? - end end class ClassVar diff --git a/src/compiler/crystal/semantic/call.cr b/src/compiler/crystal/semantic/call.cr index 33808a8427ac..6e11c94dd8f2 100644 --- a/src/compiler/crystal/semantic/call.cr +++ b/src/compiler/crystal/semantic/call.cr @@ -131,7 +131,7 @@ class Crystal::Call named_args_types = NamedArgumentType.from_args(named_args, with_literals) matches = lookup_matches_without_splat arg_types, named_args_types, with_literals - # If we checked for automatic casts, see if an ambigous call was produced + # If we checked for automatic casts, see if an ambiguous call was produced if with_literals arg_types.each &.check_restriction_exception named_args_types.try &.each &.type.check_restriction_exception @@ -247,7 +247,7 @@ class Crystal::Call matches = lookup_matches_checking_expansion(owner, signature, with_literals: with_literals) if matches.empty? && owner.class? && owner.abstract? - matches = owner.virtual_type.lookup_matches(signature) + matches = owner.virtual_type.lookup_matches(signature, analyze_all: with_literals) end if matches.empty? @@ -263,27 +263,27 @@ class Crystal::Call signature = CallSignature.new(def_name, arg_types, block, named_args_types) matches = check_tuple_indexer(owner, def_name, args, arg_types) - matches ||= lookup_matches_checking_expansion(owner, signature, search_in_parents) + matches ||= lookup_matches_checking_expansion(owner, signature, search_in_parents, with_literals: with_literals) # If we didn't find a match and this call doesn't have a receiver, # and we are not at the top level, let's try searching the top-level if matches.empty? && !obj && owner != program && search_in_toplevel - program_matches = lookup_matches_with_signature(program, signature, search_in_parents) + program_matches = lookup_matches_with_signature(program, signature, search_in_parents, with_literals) matches = program_matches unless program_matches.empty? end if matches.empty? && owner.class? && owner.abstract? && name != "super" - matches = owner.virtual_type.lookup_matches(signature) + matches = owner.virtual_type.lookup_matches(signature, analyze_all: with_literals) end if matches.empty? defined_method_missing = owner.check_method_missing(signature, self) if defined_method_missing - matches = owner.lookup_matches(signature) + matches = owner.lookup_matches(signature, analyze_all: with_literals) elsif with_scope = @with_scope defined_method_missing = with_scope.check_method_missing(signature, self) if defined_method_missing - matches = with_scope.lookup_matches(signature) + matches = with_scope.lookup_matches(signature, analyze_all: with_literals) @uses_with_scope = true end end @@ -321,9 +321,9 @@ class Crystal::Call matches = bubbling_exception do target = parent_visitor.typed_def.original_owner if search_in_parents - target.lookup_matches signature + target.lookup_matches(signature, analyze_all: with_literals) else - target.lookup_matches_without_parents signature + target.lookup_matches_without_parents(signature, analyze_all: with_literals) end end matches.each do |match| @@ -332,48 +332,36 @@ class Crystal::Call end matches else - bubbling_exception { lookup_matches_with_signature(owner, signature, search_in_parents) } + bubbling_exception { lookup_matches_with_signature(owner, signature, search_in_parents, with_literals) } end end - def lookup_matches_with_signature(owner : Program, signature, search_in_parents) + def lookup_matches_with_signature(owner : Program, signature, search_in_parents, with_literals) location = self.location if location && (filename = location.original_filename) - matches = owner.lookup_private_matches filename, signature + matches = owner.lookup_private_matches(filename, signature, analyze_all: with_literals) end if matches if matches.empty? - matches = owner.lookup_matches signature + matches = owner.lookup_matches(signature, analyze_all: with_literals) end else - matches = owner.lookup_matches signature + matches = owner.lookup_matches(signature, analyze_all: with_literals) end matches end - def lookup_matches_with_signature(owner, signature, search_in_parents) + def lookup_matches_with_signature(owner, signature, search_in_parents, with_literals) if search_in_parents - owner.lookup_matches signature + owner.lookup_matches(signature, analyze_all: with_literals) else - owner.lookup_matches_without_parents signature + owner.lookup_matches_without_parents(signature, analyze_all: with_literals) end end def instantiate(signature, matches, owner, self_type, with_literals) - if with_literals - # Now that we have all our matches, check if any of them matches exactly - # all types, assuming autocasted values will always match (because they - # matches and they were not ambiguous). If so, only keep matches up to - # that exact match. We need to do this here because with autocasting - # we consider all overloads to detect ambiguous usage. - stop_index = matches.index do |match| - signature.matches_exactly?(match, with_literals: true) - end - matches = matches[..stop_index] if stop_index - end - matches.each &.remove_literals if with_literals block = @block diff --git a/src/compiler/crystal/semantic/call_error.cr b/src/compiler/crystal/semantic/call_error.cr index 6e79ea10611e..c10bfb5681d7 100644 --- a/src/compiler/crystal/semantic/call_error.cr +++ b/src/compiler/crystal/semantic/call_error.cr @@ -92,77 +92,7 @@ class Crystal::Call end if defs.empty? - check_macro_wrong_number_of_arguments(def_name) - - owner_trace = obj.try &.find_owner_trace(owner.program, owner) - similar_name = owner.lookup_similar_def_name(def_name, self.args.size, block) - - error_msg = String.build do |msg| - if obj - could_be_local_variable = false - elsif logical_op = convert_to_logical_operator(def_name) - similar_name = logical_op - could_be_local_variable = false - elsif args.size > 0 || has_parentheses? - could_be_local_variable = false - else - # This check is for the case `a if a = 1` - similar_name = parent_visitor.lookup_similar_var_name(def_name) unless similar_name - if similar_name == def_name - could_be_local_variable = false - else - could_be_local_variable = true - end - end - - if could_be_local_variable - msg << "undefined local variable or method '#{def_name}'" - else - msg << "undefined method '#{def_name}'" - end - - owner_name = owner.is_a?(Program) ? "top-level" : owner.to_s - - if with_scope && !obj && with_scope != owner - msg << " for #{with_scope} (with ... yield) and #{owner_name} (current scope)" - else - msg << " for #{owner_name}" - end - - if def_name == "allocate" && owner.is_a?(MetaclassType) && owner.instance_type.module? - msg << colorize(" (modules cannot be instantiated)").yellow.bold - end - - if obj && obj.type != owner - msg << colorize(" (compile-time type is #{obj.type})").yellow.bold - end - - if similar_name - msg << '\n' - if similar_name == def_name - # This check is for the case `a if a = 1` - msg << "If you declared '#{def_name}' in a suffix if, declare it in a regular if for this to work. If the variable was declared in a macro it's not visible outside it)" - else - msg << "Did you mean '#{similar_name}'?" - end - end - - # Check if it's an instance variable that was never assigned a value - if obj.is_a?(InstanceVar) - scope = self.scope - ivar = scope.lookup_instance_var(obj.name) - deps = ivar.dependencies? - if deps && deps.size == 1 && deps.first.same?(program.nil_var) - similar_name = scope.lookup_similar_instance_var_name(ivar.name) - if similar_name - msg << colorize(" (#{ivar.name} was never assigned a value, did you mean #{similar_name}?)").yellow.bold - else - msg << colorize(" (#{ivar.name} was never assigned a value)").yellow.bold - end - end - end - end - raise error_msg, owner_trace + raise_undefined_method(owner, def_name, obj) end real_args_size = arg_types.size @@ -173,13 +103,6 @@ class Crystal::Call inner_exception = TypeException.for_node(similar_def, inner_msg) end - if owner_trace - owner_trace.inner = inner_exception - inner_exception = nil - else - owner_trace = inner_exception - end - defs_matching_args_size = defs.select do |a_def| min_size, max_size = a_def.min_max_args_sizes min_size <= real_args_size <= max_size @@ -187,44 +110,7 @@ class Crystal::Call # Don't say "wrong number of arguments" when there are named args in this call if defs_matching_args_size.empty? && !named_args_types - all_arguments_sizes = [] of Int32 - min_splat = Int32::MAX - defs.each do |a_def| - min_size, max_size = a_def.min_max_args_sizes - if max_size == Int32::MAX - min_splat = Math.min(min_size, min_splat) - all_arguments_sizes.push min_splat - else - min_size.upto(max_size) do |size| - all_arguments_sizes.push size - end - end - end - all_arguments_sizes.uniq!.sort! - - raise(String.build do |str| - unless check_single_def_error_message(defs, named_args_types, str) - str << "wrong number of arguments for '" - str << full_name(owner, def_name) - str << "' (given " - str << real_args_size - str << ", expected " - - # If we have 2, 3, 4, show it as 2..4 - if all_arguments_sizes.size > 1 && all_arguments_sizes.last - all_arguments_sizes.first == all_arguments_sizes.size - 1 - str << all_arguments_sizes.first - str << ".." - str << all_arguments_sizes.last - else - all_arguments_sizes.join ", ", str - end - - str << '+' if min_splat != Int32::MAX - str << ")\n" - end - str << "Overloads are:" - append_matches(defs, arg_types, str) - end, inner: inner_exception) + raise_matches_not_found_named_args(owner, def_name, defs, real_args_size, arg_types, named_args_types, inner_exception) end if defs_matching_args_size.size > 0 @@ -257,6 +143,8 @@ class Crystal::Call if args.size == 1 && args.first.type.includes_type?(program.nil) owner_trace = args.first.find_owner_trace(program, program.nil) + else + owner_trace = inner_exception end arg_names = [] of Array(String) @@ -268,7 +156,7 @@ class Crystal::Call msg << " with type" msg << 's' if arg_types.size > 1 || named_args_types msg << ' ' - arg_types.join(", ", msg) + arg_types.join(msg, ", ") end if named_args_types @@ -283,7 +171,7 @@ class Crystal::Call msg << '\n' defs.each do |a_def| - arg_names.try &.push a_def.args.map(&.name) + arg_names << a_def.args.map(&.name) end end @@ -323,6 +211,117 @@ class Crystal::Call raise message, owner_trace end + private def raise_undefined_method(owner, def_name, obj) + check_macro_wrong_number_of_arguments(def_name) + + owner_trace = obj.try &.find_owner_trace(owner.program, owner) + similar_name = owner.lookup_similar_def_name(def_name, self.args.size, block) + + error_msg = String.build do |msg| + if obj + could_be_local_variable = false + elsif logical_op = convert_to_logical_operator(def_name) + similar_name = logical_op + could_be_local_variable = false + elsif args.size > 0 || has_parentheses? + could_be_local_variable = false + else + # This check is for the case `a if a = 1` + similar_name = parent_visitor.lookup_similar_var_name(def_name) unless similar_name + could_be_local_variable = (similar_name != def_name) + end + + if could_be_local_variable + msg << "undefined local variable or method '#{def_name}'" + else + msg << "undefined method '#{def_name}'" + end + + owner_name = owner.is_a?(Program) ? "top-level" : owner.to_s + + if with_scope && !obj && with_scope != owner + msg << " for #{with_scope} (with ... yield) and #{owner_name} (current scope)" + else + msg << " for #{owner_name}" + end + + if def_name == "allocate" && owner.is_a?(MetaclassType) && owner.instance_type.module? + msg << colorize(" (modules cannot be instantiated)").yellow.bold + end + + if obj && obj.type != owner + msg << colorize(" (compile-time type is #{obj.type})").yellow.bold + end + + if similar_name + msg << '\n' + if similar_name == def_name + # This check is for the case `a if a = 1` + msg << "If you declared '#{def_name}' in a suffix if, declare it in a regular if for this to work. If the variable was declared in a macro it's not visible outside it)" + else + msg << "Did you mean '#{similar_name}'?" + end + end + + # Check if it's an instance variable that was never assigned a value + if obj.is_a?(InstanceVar) + scope = self.scope + ivar = scope.lookup_instance_var(obj.name) + deps = ivar.dependencies? + if deps && deps.size == 1 && deps.first.same?(program.nil_var) + similar_name = scope.lookup_similar_instance_var_name(ivar.name) + if similar_name + msg << colorize(" (#{ivar.name} was never assigned a value, did you mean #{similar_name}?)").yellow.bold + else + msg << colorize(" (#{ivar.name} was never assigned a value)").yellow.bold + end + end + end + end + raise error_msg, owner_trace + end + + private def raise_matches_not_found_named_args(owner, def_name, defs, real_args_size, arg_types, named_args_types, inner_exception) + all_arguments_sizes = [] of Int32 + min_splat = Int32::MAX + defs.each do |a_def| + min_size, max_size = a_def.min_max_args_sizes + if max_size == Int32::MAX + min_splat = Math.min(min_size, min_splat) + all_arguments_sizes.push min_splat + else + min_size.upto(max_size) do |size| + all_arguments_sizes.push size + end + end + end + all_arguments_sizes.uniq!.sort! + + raise(String.build do |str| + unless check_single_def_error_message(defs, named_args_types, str) + str << "wrong number of arguments for '" + str << full_name(owner, def_name) + str << "' (given " + str << real_args_size + str << ", expected " + + # If we have 2, 3, 4, show it as 2..4 + if all_arguments_sizes.size > 1 && all_arguments_sizes.last - all_arguments_sizes.first == all_arguments_sizes.size - 1 + str << all_arguments_sizes.first + str << ".." + str << all_arguments_sizes.last + else + all_arguments_sizes.join str, ", " + end + + str << '+' if min_splat != Int32::MAX + str << ")\n" + end + str << "Overloads are:" + append_matches(defs, arg_types, str) + end, inner: inner_exception) + end + def convert_to_logical_operator(def_name) case def_name when "and"; "&&" diff --git a/src/compiler/crystal/semantic/class_vars_initializer_visitor.cr b/src/compiler/crystal/semantic/class_vars_initializer_visitor.cr index 700666a9183b..c00a318fc81e 100644 --- a/src/compiler/crystal/semantic/class_vars_initializer_visitor.cr +++ b/src/compiler/crystal/semantic/class_vars_initializer_visitor.cr @@ -73,7 +73,7 @@ module Crystal (class_var_type = class_var.type?) cloned_node = node.clone cloned_node.accept MainVisitor.new(self) - if casted_value = MainVisitor.check_automatic_cast(cloned_node, class_var_type) + if casted_value = MainVisitor.check_automatic_cast(@program, cloned_node, class_var_type) node = initializer.node = casted_value end end diff --git a/src/compiler/crystal/semantic/cleanup_transformer.cr b/src/compiler/crystal/semantic/cleanup_transformer.cr index e6ed1543c3c0..d7e03c97d29e 100644 --- a/src/compiler/crystal/semantic/cleanup_transformer.cr +++ b/src/compiler/crystal/semantic/cleanup_transformer.cr @@ -172,10 +172,10 @@ module Crystal end def transform(node : Case) - @exhaustiveness_checker.check(node) + @exhaustiveness_checker.check(node) if node.exhaustive? if expanded = node.expanded - unless node.else + if node.exhaustive? replace_unreachable_if_needed(node, expanded) end @@ -188,8 +188,7 @@ module Crystal # If any of the types checked in `case` is an enum, it can happen that # the unreachable can be reached by doing `SomeEnum.new(some_value)`. # In that case we replace the Unreachable node with `raise "..."`. - # In the future we should disallow creating such values unless the - # enum is marked as "open". + # In the future we should disallow creating such values. def replace_unreachable_if_needed(node, expanded) cond = node.cond return unless cond @@ -316,7 +315,7 @@ module Crystal def transform(node : Global) if expanded = node.expanded - return expanded + return expanded.transform self end node @@ -327,6 +326,8 @@ module Crystal return expanded.transform self end + @program.check_call_to_deprecated_method(node) + # Need to transform these manually because node.block doesn't # need to be transformed if it has a fun_literal # ~~~ @@ -791,7 +792,7 @@ module Crystal end if expanded = node.expanded - return expanded + return expanded.transform self end node diff --git a/src/compiler/crystal/semantic/exception.cr b/src/compiler/crystal/semantic/exception.cr index 39ba11028a78..223f1fa3acef 100644 --- a/src/compiler/crystal/semantic/exception.cr +++ b/src/compiler/crystal/semantic/exception.cr @@ -91,16 +91,16 @@ module Crystal io.flush end - def to_s_with_source(source, io) - append_to_s source, io + def to_s_with_source(io : IO, source) + append_to_s io, source end - def append_to_s(source, io) + def append_to_s(io : IO, source) inner = @inner unless @error_trace || inner.is_a? MethodTraceException if inner && inner.has_location? - return inner.append_to_s(source, io) + return inner.append_to_s(io, source) end end @@ -134,7 +134,7 @@ module Crystal return unless inner.has_location? io << "\n\n" io << '\n' unless inner.is_a? MethodTraceException - inner.append_to_s source, io + inner.append_to_s io, source end end @@ -183,8 +183,8 @@ module Crystal def to_json_single(json) end - def to_s_with_source(source, io) - append_to_s(source, io) + def to_s_with_source(io : IO, source) + append_to_s(io, source) end def has_trace? @@ -195,7 +195,7 @@ module Crystal @nil_reason || has_trace? && @show end - def append_to_s(source, io) + def append_to_s(io : IO, source) nil_reason = @nil_reason if !@show diff --git a/src/compiler/crystal/semantic/exhaustiveness_checker.cr b/src/compiler/crystal/semantic/exhaustiveness_checker.cr index be8cb3362f6c..bc06f3b8dce2 100644 --- a/src/compiler/crystal/semantic/exhaustiveness_checker.cr +++ b/src/compiler/crystal/semantic/exhaustiveness_checker.cr @@ -3,13 +3,7 @@ struct Crystal::ExhaustivenessChecker end def check(node : Case) - # If there's an else clause we don't need to check anything - return if node.else - - cond = node.cond - - # No condition means it's just like a series of if/else - return unless cond + cond = node.cond.not_nil! if cond.is_a?(TupleLiteral) check_tuple_exp(node, cond) @@ -22,7 +16,7 @@ struct Crystal::ExhaustivenessChecker cond_type = cond.type? # No type on condition means we couldn't type it so we can't - # check exhasutiveness. + # check exhaustiveness. return unless cond_type # Compute all types that we must cover. @@ -33,17 +27,12 @@ struct Crystal::ExhaustivenessChecker # Compute all the targets that we must cover targets = cond_types.map { |cond_type| compute_target(cond_type) } + # Is any type a @[Flags] enum? + flags_enum = cond_types.find { |type| type.is_a?(EnumType) && type.flags? } + # Are all patterns Path types? all_patterns_are_types = true - # Are all patterns things that we can handle? - # For example an integer literal is something that we don't - # take into account for exhaustiveness. - all_provable_patterns = true - - # Is any type a @[Flags] enum? - has_flags_enum = cond_types.any? { |type| type.is_a?(EnumType) && type.flags? } - # Start checking each `when`... node.whens.each do |a_when| a_when.conds.each do |when_cond| @@ -53,11 +42,7 @@ struct Crystal::ExhaustivenessChecker all_patterns_are_types = false end - if pattern - targets.each &.cover(pattern) - else - all_provable_patterns = false - end + targets.each &.cover(pattern) end end @@ -87,37 +72,34 @@ struct Crystal::ExhaustivenessChecker MSG when EnumTarget node.raise <<-MSG - case is not exhaustive for enum #{single_target.type}. + case is not exhaustive for enum #{single_target.type}. - Missing members: - - #{single_target.members.map(&.name).join("\n - ")} - MSG + Missing members: + - #{single_target.members.map(&.name).join("\n - ")} + MSG else # No specific error messages for non-single types end - if all_provable_patterns && !has_flags_enum - node.raise <<-MSG - case is not exhaustive. + msg = <<-MSG + case is not exhaustive. - Missing cases: - - #{targets.flat_map(&.missing_cases).join("\n - ")} - MSG - end + Missing cases: + - #{targets.flat_map(&.missing_cases).join("\n - ")} + MSG - # Otherwise we can't prove exhaustiveness and an `else` clause is required - node.raise <<-MSG - can't prove case is exhaustive. + if flags_enum + msg += "\n\n" + flags_enum_message(flags_enum) + end - Please add an `else` clause. - MSG + node.raise msg end private def check_tuple_exp(node, cond) elements = cond.elements # No type on condition means we couldn't type it so we can't - # check exhasutiveness. + # check exhaustiveness. return unless elements.all? &.type? element_types = elements.map &.type @@ -126,13 +108,19 @@ struct Crystal::ExhaustivenessChecker expand_types(element_type) end - # Compute all the targets that we must cover - targets = compute_targets(all_expanded_types) + # Is any type a @[Flags] enum? + flags_enum = nil + all_expanded_types.each do |types| + types.each do |type| + if type.is_a?(EnumType) && type.flags? + flags_enum = type + break + end + end + break if flags_enum + end - # Are all patterns things that we can handle? - # For example an integer literal is something that we don't - # take into account for exhaustiveness. - all_provable_patterns = true + targets = compute_targets(all_expanded_types) # Start checking each `when`... node.whens.each do |a_when| @@ -142,12 +130,7 @@ struct Crystal::ExhaustivenessChecker when_pattern(when_cond_exp) end - if patterns.all? - patterns = patterns.map &.not_nil! - targets.each &.cover(patterns, 0) - else - all_provable_patterns = false - end + targets.each &.cover(patterns, 0) else # Not a tuple literal so we don't care # TODO: one could put `Tuple` or `Object` here and that would make @@ -162,26 +145,29 @@ struct Crystal::ExhaustivenessChecker # If we covered all types, we are done. return if targets.empty? - # If all patterns are stuff we can handle, show the missing cases - if all_provable_patterns - missing_cases = targets - .flat_map(&.missing_cases) - .map { |cases| "{#{cases}}" } - .join("\n - ") + missing_cases = targets + .flat_map(&.missing_cases) + .map { |cases| "{#{cases}}" } + .join("\n - ") - node.raise <<-MSG - case is not exhaustive. + msg = <<-MSG + case is not exhaustive. - Missing cases: - - #{missing_cases} - MSG + Missing cases: + - #{missing_cases} + MSG + + if flags_enum + msg += "\n\n" + flags_enum_message(flags_enum) end - # Otherwise we can't prove exhaustiveness and an `else` clause is required - node.raise <<-MSG - can't prove case is exhaustive. + node.raise msg + end - Please add an `else` clause. + private def flags_enum_message(flags_enum) + <<-MSG + Note that @[Flags] enum can't be proved to be exhaustive by matching against enum members. + In particular, the enum #{flags_enum} can't be proved to be exhaustive like that. MSG end @@ -193,7 +179,7 @@ struct Crystal::ExhaustivenessChecker end end - # Retuens an array of all the types inside `type`: + # Returns an array of all the types inside `type`: # for unions it's all the union types, otherwise it's just that type. private def expand_types(type) if type.is_a?(UnionType) @@ -236,20 +222,26 @@ struct Crystal::ExhaustivenessChecker # if it's an enum member, try to remove it from the targets. EnumMemberPattern.new(target_const) else - nil + when_cond.raise "can't use constant values in exhaustive case, only constant types" end + when Generic + TypePattern.new(when_cond.type.devirtualize) when Call + obj = when_cond.obj + # Check if it's something like `.foo?` to remove that member from the ones # we must cover. # Note: a user could override the meaning of such methods. # In the future it would be wise to mark these as non-redefinable # so this checks are sounds. - if when_cond.obj.is_a?(ImplicitObj) && - when_cond.args.empty? && when_cond.named_args.nil? && - !when_cond.block && !when_cond.block_arg && when_cond.name.ends_with?('?') + if obj.is_a?(ImplicitObj) && when_cond.name.ends_with?('?') EnumMemberNamePattern.new(when_cond.name.rchop) + elsif obj.is_a?(Path) && when_cond.name == "class" + TypePattern.new(obj.type.metaclass.devirtualize) + elsif obj.is_a?(Generic) && when_cond.name == "class" + TypePattern.new(obj.type.metaclass.devirtualize) else - nil + raise "Bug: unknown pattern in exhaustive case" end when BoolLiteral BoolPattern.new(when_cond.value) @@ -258,7 +250,7 @@ struct Crystal::ExhaustivenessChecker when Underscore UnderscorePattern.new else - nil + raise "Bug: unknown pattern in exhaustive case" end end diff --git a/src/compiler/crystal/semantic/flags.cr b/src/compiler/crystal/semantic/flags.cr index 975a83444c6e..d412dfbc011f 100644 --- a/src/compiler/crystal/semantic/flags.cr +++ b/src/compiler/crystal/semantic/flags.cr @@ -1,5 +1,6 @@ class Crystal::Program @flags : Set(String)? + @host_flags : Set(String)? # Returns the flags for this program. By default these # are computed from the target triple (for example x86_64, @@ -11,6 +12,10 @@ class Crystal::Program @flags ||= flags_for_target(codegen_target) end + def host_flags + @host_flags ||= flags_for_target(Config.host_target) + end + # Returns `true` if *name* is in the program's flags. def has_flag?(name : String) flags.includes?(name) @@ -20,29 +25,30 @@ class Crystal::Program codegen_target.pointer_bit_width == 64 end - private def flags_for_target(codegen_target) + private def flags_for_target(target) flags = Set(String).new - flags.add codegen_target.architecture - flags.add codegen_target.vendor - flags.concat codegen_target.environment_parts + flags.add target.architecture + flags.add target.vendor + flags.concat target.environment_parts - flags.add "bits#{codegen_target.pointer_bit_width}" + flags.add "bits#{target.pointer_bit_width}" - flags.add "armhf" if codegen_target.armhf? + flags.add "armhf" if target.armhf? - flags.add "unix" if codegen_target.unix? - flags.add "win32" if codegen_target.win32? + flags.add "unix" if target.unix? + flags.add "win32" if target.win32? - flags.add "darwin" if codegen_target.macos? - if codegen_target.freebsd? + flags.add "darwin" if target.macos? + if target.freebsd? flags.add "freebsd" - flags.add "freebsd#{codegen_target.freebsd_version}" + flags.add "freebsd#{target.freebsd_version}" end - flags.add "openbsd" if codegen_target.openbsd? - flags.add "dragonfly" if codegen_target.dragonfly? + flags.add "netbsd" if target.netbsd? + flags.add "openbsd" if target.openbsd? + flags.add "dragonfly" if target.dragonfly? - flags.add "bsd" if codegen_target.bsd? + flags.add "bsd" if target.bsd? flags end diff --git a/src/compiler/crystal/semantic/instance_vars_initializer_visitor.cr b/src/compiler/crystal/semantic/instance_vars_initializer_visitor.cr index 91e37e6aa16a..b67c9f3f0d80 100644 --- a/src/compiler/crystal/semantic/instance_vars_initializer_visitor.cr +++ b/src/compiler/crystal/semantic/instance_vars_initializer_visitor.cr @@ -98,7 +98,7 @@ class Crystal::InstanceVarsInitializerVisitor < Crystal::SemanticVisitor (scope_initializer = scope_initializers[index]) cloned_value = value.clone cloned_value.accept MainVisitor.new(program) - if casted_value = MainVisitor.check_automatic_cast(cloned_value, scope.lookup_instance_var(i.target.name).type) + if casted_value = MainVisitor.check_automatic_cast(@program, cloned_value, scope.lookup_instance_var(i.target.name).type) scope_initializer.value = casted_value next end diff --git a/src/compiler/crystal/semantic/literal_expander.cr b/src/compiler/crystal/semantic/literal_expander.cr index f54dda1b8f4e..af3002efe2da 100644 --- a/src/compiler/crystal/semantic/literal_expander.cr +++ b/src/compiler/crystal/semantic/literal_expander.cr @@ -465,7 +465,11 @@ module Crystal a_if = wh_if end - a_if.not_nil!.else = node.else || Unreachable.new + if node.exhaustive? + a_if.not_nil!.else = node.else || Unreachable.new + elsif node_else = node.else + a_if.not_nil!.else = node_else + end final_if = final_if.not_nil! final_exp = if assigns && !assigns.empty? @@ -524,7 +528,7 @@ module Crystal call = Call.new(channel, call_name, call_args).at(node) multi = MultiAssign.new(targets, [call] of ASTNode) case_cond = Var.new(index_name).at(node) - a_case = Case.new(case_cond, case_whens, case_else).at(node) + a_case = Case.new(case_cond, case_whens, case_else, exhaustive: false).at(node) Expressions.from([multi, a_case] of ASTNode).at(node) end @@ -631,11 +635,11 @@ module Crystal case obj when Path if cond.name == "class" - return IsA.new(right_side, Metaclass.new(obj.clone).at(obj)) + return IsA.new(right_side, Metaclass.new(obj).at(obj)) end when Generic if cond.name == "class" - return IsA.new(right_side, Metaclass.new(obj.clone).at(obj)) + return IsA.new(right_side, Metaclass.new(obj).at(obj)) end else # no special treatment diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index 238a353dfe6e..720ae39a5397 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -58,11 +58,11 @@ module Crystal property yield_vars : Array(Var)? # In vars we store the types of variables as we traverse the nodes. - # These type are not cummulative: if you do `x = 1`, 'x' will have + # These type are not cumulative: if you do `x = 1`, 'x' will have # type Int32. Then if you do `x = false`, 'x' will have type Bool. getter vars - # Here we store the cummulative types of variables as we traverse the nodes. + # Here we store the cumulative types of variables as we traverse the nodes. getter meta_vars : MetaVars property is_initialize : Bool property exception_handler_vars : MetaVars? = nil @@ -127,7 +127,7 @@ module Crystal # We initialize meta_vars from vars given in the constructor. # We store those meta vars either in the typed def or in the program - # so the codegen phase knows the cummulative types to do allocas. + # so the codegen phase knows the cumulative types to do allocas. unless meta_vars if typed_def = @typed_def meta_vars = typed_def.vars = MetaVars.new @@ -843,7 +843,7 @@ module Crystal def type_assign(target : InstanceVar, value, node) # Check if this is an instance variable initializer unless @scope - # `InstanceVar` assignment appered in block is not checked + # `InstanceVar` assignment appeared in block is not checked # by `Crystal::InstanceVarsInitializerVisitor` because this block # may be passed to a macro. So, it checks here. if current_type.is_a?(Program) || current_type.is_a?(FileModule) @@ -977,23 +977,27 @@ module Crystal # See if we can automatically cast the value if the types don't exactly match def check_automatic_cast(value, var_type, assign = nil) - MainVisitor.check_automatic_cast(value, var_type, assign) + MainVisitor.check_automatic_cast(@program, value, var_type, assign) end - def self.check_automatic_cast(value, var_type, assign = nil) - if value.is_a?(NumberLiteral) && value.type != var_type && (var_type.is_a?(IntegerType) || var_type.is_a?(FloatType)) - if value.can_be_autocast_to?(var_type) - value.type = var_type - value.kind = var_type.kind + def self.check_automatic_cast(program, value, var_type, assign = nil) + if value.is_a?(NumberLiteral) && value.type != var_type + literal_type = NumberLiteralType.new(program, value) + restricted = literal_type.restrict(var_type, MatchContext.new(value.type, value.type)) + if restricted.is_a?(IntegerType) || restricted.is_a?(FloatType) + value.type = restricted + value.kind = restricted.kind assign.value = value if assign return value end - elsif value.is_a?(SymbolLiteral) && var_type.is_a?(EnumType) - member = var_type.find_member(value.value) - if member + elsif value.is_a?(SymbolLiteral) && value.type != var_type + literal_type = SymbolLiteralType.new(program, value) + restricted = literal_type.restrict(var_type, MatchContext.new(value.type, value.type)) + if restricted.is_a?(EnumType) + member = restricted.find_member(value.value).not_nil! path = Path.new(member.name) path.target_const = member - path.type = var_type + path.type = restricted value = path assign.value = value if assign return value @@ -1284,7 +1288,7 @@ module Crystal if expand_macro(node) # It can happen that this call is inside an ArrayLiteral or HashLiteral, # was expanded but isn't bound to the expansion because the call (together - # with its expantion) was cloned. + # with its expansion) was cloned. if (expanded = node.expanded) && (!node.dependencies? || !node.type?) node.bind_to(expanded) end @@ -3161,6 +3165,7 @@ module Crystal def visit(node : Unreachable) node.type = @program.no_return + @unreachable = true end # # Helpers @@ -3207,6 +3212,8 @@ module Crystal end def check_self_closured + return if @typeof_nest > 0 + scope = @scope return unless scope diff --git a/src/compiler/crystal/semantic/method_lookup.cr b/src/compiler/crystal/semantic/method_lookup.cr index 0b13572ec657..8b7ac5672f3a 100644 --- a/src/compiler/crystal/semantic/method_lookup.cr +++ b/src/compiler/crystal/semantic/method_lookup.cr @@ -1,5 +1,55 @@ require "../types" +# Looking up matches involves two steps: +# +# 1. Lookup is done with autocasting disabled. +# +# In this scenario as soon as we find an exact match we don't look at other +# overloads because the exact match will prevent them from being considered. +# +# If no matches are found we try again but this time with autocasting enabled. +# In `semantic/call.cr` this is when `with_literals` is `true`, and this is when +# `analyze_all` will be `true` here. +# +# 2. Lookup is done with autocasting enabled. +# +# In this mode the types for NumberLiteral and SymbolLiteral are not the usual +# types but instead the special NumberLiteralType and SymbolLiteralType. +# +# In this mode we also need to stop as soon as we find an exact match +# (which just means when the first overload matches with autocasting, which +# is for example when passing 1 to an Int64 restriction) but we still need +# to analyze all possible methods in case there's an ambiguity. For example: +# +# ``` +# def foo(x : Int64) +# end +# +# def foo(x : Int8) +# end +# +# foo(1) +# ``` +# +# In the example above we can't just stop at the first overload because +# we need to analyze the second overload to find out that the call is ambiguous. +# +# However, consider this: +# +# ``` +# def foo(x : Int64) +# end +# +# def foo(x : *Int64) +# end +# +# foo(1) +# ``` +# +# In this case there's no ambiguity: 1 means `Int64`. However, the first overload +# is an exact match and there's no need to consider the second overload in the +# multidispatch. However, we do need to analyze it to check if there's an ambiguity. + module Crystal record NamedArgumentType, name : String, type : Type do def self.from_args(named_args : Array(NamedArgument)?, with_literals = false) @@ -16,8 +66,8 @@ module Crystal named_args : Array(NamedArgumentType)? class Type - def lookup_matches(signature, owner = self, path_lookup = self, matches_array = nil) - matches = lookup_matches_without_parents(signature, owner, path_lookup, matches_array) + def lookup_matches(signature, owner = self, path_lookup = self, matches_array = nil, analyze_all = false) + matches = lookup_matches_without_parents(signature, owner, path_lookup, matches_array, analyze_all: analyze_all) return matches if matches.cover_all? matches_array = matches.matches @@ -38,7 +88,7 @@ module Crystal # and can be known by invoking `lookup_new_in_ancestors?` if my_parents && !(is_new && !lookup_new_in_ancestors?) my_parents.each do |parent| - matches = parent.lookup_matches(signature, owner, parent, matches_array) + matches = parent.lookup_matches(signature, owner, parent, matches_array, analyze_all: analyze_all) if matches.cover_all? return matches else @@ -55,10 +105,12 @@ module Crystal Matches.new(matches_array, cover, owner, false) end - def lookup_matches_without_parents(signature, owner = self, path_lookup = self, matches_array = nil) + def lookup_matches_without_parents(signature, owner = self, path_lookup = self, matches_array = nil, analyze_all = false) if defs = self.defs.try &.[signature.name]? context = MatchContext.new(owner, path_lookup) + exact_match = nil + defs.each do |item| next if item.def.abstract? @@ -72,6 +124,8 @@ module Crystal match = signature.match(item, context) + next if exact_match + if match matches_array ||= [] of Match matches_array << match @@ -81,7 +135,8 @@ module Crystal # a function type with return T can be transpass a restriction of a function # with the same arguments but which returns Void. if signature.matches_exactly?(match) - return Matches.new(matches_array, true, owner) + exact_match = Matches.new(matches_array, true, owner) + break unless analyze_all end context = MatchContext.new(owner, path_lookup) @@ -90,13 +145,17 @@ module Crystal context.def_free_vars = nil end end + + if exact_match + return exact_match + end end Matches.new(matches_array, Cover.create(signature, matches_array), owner) end - def lookup_matches_with_modules(signature, owner = self, path_lookup = self, matches_array = nil) - matches = lookup_matches_without_parents(signature, owner, path_lookup, matches_array) + def lookup_matches_with_modules(signature, owner = self, path_lookup = self, matches_array = nil, analyze_all = false) + matches = lookup_matches_without_parents(signature, owner, path_lookup, matches_array, analyze_all: analyze_all) return matches unless matches.empty? is_new = owner.metaclass? && signature.name == "new" @@ -115,7 +174,7 @@ module Crystal my_parents.each do |parent| break unless parent.module? - matches = parent.lookup_matches_with_modules(signature, owner, parent, matches_array) + matches = parent.lookup_matches_with_modules(signature, owner, parent, matches_array, analyze_all: analyze_all) return matches unless matches.empty? end end @@ -246,7 +305,7 @@ module Crystal if a_def.double_splat match_arg_type = named_arg.type - # If there's a restrction on the double splat, check that it matches + # If there's a restriction on the double splat, check that it matches if double_splat_restriction if double_splat_entries double_splat_entries << named_arg @@ -304,10 +363,6 @@ module Crystal def matches_exactly?(match : Match, *, with_literals : Bool = false) arg_types_equal = self.arg_types.equals?(match.arg_types) do |x, y| - if with_literals && x.is_a?(LiteralType) - x = x.match || x.remove_literal - end - x.compatible_with?(y) end if (match_named_args = match.named_arg_types) && (signature_named_args = self.named_args) && @@ -315,12 +370,7 @@ module Crystal match_named_args = match_named_args.sort_by &.name signature_named_args = signature_named_args.sort_by &.name named_arg_types_equal = signature_named_args.equals?(match_named_args) do |x, y| - x_type = x.type - if with_literals && x_type.is_a?(LiteralType) - x_type = x_type.match || x_type.remove_literal - end - - x.name == y.name && x_type.compatible_with?(y.type) + x.name == y.name && x.type.compatible_with?(y.type) end else named_arg_types_equal = !match.named_arg_types && !self.named_args @@ -341,11 +391,11 @@ module Crystal type end - def lookup_matches(signature, owner = self, path_lookup = self) + def lookup_matches(signature, owner = self, path_lookup = self, analyze_all = false) is_new = virtual_metaclass? && signature.name == "new" base_type_lookup = virtual_lookup(base_type) - base_type_matches = base_type_lookup.lookup_matches(signature, self) + base_type_matches = base_type_lookup.lookup_matches(signature, self, analyze_all: analyze_all) # If there are no subclasses no need to look further if leaf? @@ -369,7 +419,7 @@ module Crystal subtype_virtual_lookup = virtual_lookup(subtype.virtual_type) # Check matches but without parents: only included modules - subtype_matches = subtype_lookup.lookup_matches_with_modules(signature, subtype_virtual_lookup, subtype_virtual_lookup) + subtype_matches = subtype_lookup.lookup_matches_with_modules(signature, subtype_virtual_lookup, subtype_virtual_lookup, analyze_all: analyze_all) # For Foo+.class#new we need to check that this subtype doesn't define # an incompatible initialize: if so, we return empty matches, because @@ -390,7 +440,7 @@ module Crystal base_type_matches.each do |base_type_match| if base_type_match.def.macro_def? # We need to copy each submatch if it's a macro def - full_subtype_matches = subtype_lookup.lookup_matches(signature, subtype_virtual_lookup, subtype_virtual_lookup) + full_subtype_matches = subtype_lookup.lookup_matches(signature, subtype_virtual_lookup, subtype_virtual_lookup, analyze_all: analyze_all) full_subtype_matches.each do |full_subtype_match| cloned_def = full_subtype_match.def.clone cloned_def.macro_owner = full_subtype_match.def.macro_owner diff --git a/src/compiler/crystal/semantic/restrictions.cr b/src/compiler/crystal/semantic/restrictions.cr index 1e862e60c1b7..2a8b616702ea 100644 --- a/src/compiler/crystal/semantic/restrictions.cr +++ b/src/compiler/crystal/semantic/restrictions.cr @@ -1232,14 +1232,18 @@ module Crystal literal.type.restrict(other, context) end else - type = super(other, context) || - literal.type.restrict(other, context) + type = literal.type.restrict(other, context) || + super(other, context) if type == self type = @match || literal.type end type end end + + def compatible_with?(type) + literal.type == type || literal.can_be_autocast_to?(type) + end end class SymbolLiteralType @@ -1256,14 +1260,25 @@ module Crystal literal.type.restrict(other, context) end else - type = super(other, context) || - literal.type.restrict(other, context) + type = literal.type.restrict(other, context) || + super(other, context) if type == self type = @match || literal.type end type end end + + def compatible_with?(type) + case type + when SymbolType + true + when EnumType + !!(type.find_member(literal.value)) + else + false + end + end end end diff --git a/src/compiler/crystal/semantic/semantic_visitor.cr b/src/compiler/crystal/semantic/semantic_visitor.cr index a165a1659352..852c1b29fcee 100644 --- a/src/compiler/crystal/semantic/semantic_visitor.cr +++ b/src/compiler/crystal/semantic/semantic_visitor.cr @@ -68,6 +68,24 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor node.expanded = expanded node.bind_to(expanded) false + rescue ex : CrystalPath::NotFoundError + message = "can't find file '#{ex.filename}'" + notes = [] of String + + # FIXME: as(String) should not be necessary + if ex.filename.as(String).starts_with? '.' + if relative_to + message += " relative to '#{relative_to}'" + end + else + notes << <<-NOTE + If you're trying to require a shard: + - Did you remember to run `shards install`? + - Did you make sure you're running the compiler in the same directory as your shard.yml? + NOTE + end + + node.raise "#{message}\n\n#{notes.join("\n")}" rescue ex : Crystal::Exception node.raise "while requiring \"#{node.string}\"", ex rescue ex @@ -349,7 +367,7 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor def initialize(@doc) end - def visit(node : ClassDef | ModuleDef | EnumDef | Def | FunDef | Alias | Assign | Call) + def visit(node : ClassDef | ModuleDef | EnumDef | Def | FunDef | AnnotationDef | Alias | Assign | Call) node.doc ||= @doc false end @@ -429,7 +447,9 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor def process_annotations(annotations) annotations.try &.each do |ann| - yield lookup_annotation(ann), ann + annotation_type = lookup_annotation(ann) + validate_annotation(annotation_type, ann) + yield annotation_type, ann end end @@ -456,6 +476,21 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor type end + def validate_annotation(annotation_type, ann) + case annotation_type + when @program.deprecated_annotation + # Check whether a DeprecatedAnnotation can be built. + # There is no need to store it, but enforcing + # arguments makes sense here. + DeprecatedAnnotation.from(ann) + when @program.experimental_annotation + # ditto DeprecatedAnnotation + ExperimentalAnnotation.from(ann) + else + # go on + end + end + def check_class_var_annotations thread_local = false process_annotations(@annotations) do |annotation_type, ann| diff --git a/src/compiler/crystal/semantic/top_level_visitor.cr b/src/compiler/crystal/semantic/top_level_visitor.cr index e712c5eb1dce..3ce013f6bc43 100644 --- a/src/compiler/crystal/semantic/top_level_visitor.cr +++ b/src/compiler/crystal/semantic/top_level_visitor.cr @@ -192,7 +192,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor type.add_annotation(annotation_type, ann) end - attach_doc type, node + attach_doc type, node, annotations pushing_type(type) do run_hooks(hook_type(superclass), type, :inherited, node) if created_new_type @@ -235,7 +235,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor node.resolved_type = type - attach_doc type, node + attach_doc type, node, annotations process_annotations(annotations) do |annotation_type, ann| type.add_annotation(annotation_type, ann) @@ -262,7 +262,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor scope.types[name] = type end - attach_doc type, node + attach_doc type, node, annotations: nil false end @@ -270,6 +270,8 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor def visit(node : Alias) check_outside_exp node, "declare alias" + annotations = read_annotations + scope, name, existing_type = lookup_type_def(node) if existing_type @@ -281,7 +283,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor end alias_type = AliasType.new(@program, scope, name, node.value) - attach_doc alias_type, node + attach_doc alias_type, node, annotations scope.types[name] = alias_type alias_type.private = true if node.visibility.private? @@ -294,6 +296,13 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor def visit(node : Macro) check_outside_exp node, "declare macro" + annotations = read_annotations + process_annotations(annotations) do |annotation_type, ann| + node.add_annotation(annotation_type, ann) + end + node.doc ||= annotations_doc(annotations) + check_ditto node, node.location + node.set_type @program.nil if node.name == "finished" @@ -455,7 +464,17 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor process_annotations(annotations) do |annotation_type, ann| case annotation_type when @program.link_annotation - type.add_link_annotation(LinkAnnotation.from(ann)) + link_annotation = LinkAnnotation.from(ann) + + if link_annotation.static? + @program.report_warning(ann, "specifying static linking for individual libraries is deprecated") + end + + if ann.args.size > 1 + @program.report_warning(ann, "using non-named arguments for Link annotations is deprecated") + end + + type.add_link_annotation(link_annotation) when @program.call_convention_annotation type.call_convention = parse_call_convention(ann, type.call_convention) else @@ -530,8 +549,6 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor annotations = read_annotations - annotations_doc = annotations_doc(annotations) - scope, name, enum_type = lookup_type_def(node) if enum_type @@ -561,9 +578,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor end node.resolved_type = enum_type - attach_doc enum_type, node - - enum_type.doc ||= annotations_doc(annotations) + attach_doc enum_type, node, annotations pushing_type(enum_type) do counter = enum_type.flags? ? 1 : 0 @@ -741,6 +756,8 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor check_outside_exp node, "declare constant" @exp_nest += 1 + annotations = read_annotations + scope, name = lookup_type_def_name(target) if current_type.is_a?(Program) scope = program.check_private(target) || scope @@ -755,7 +772,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor const.private = true if target.visibility.private? check_ditto node, node.location - attach_doc const, node + attach_doc const, node, annotations scope.types[name] = const @@ -960,25 +977,6 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor false end - def process_lib_annotations - link_annotations = nil - call_convention = nil - - process_annotations do |annotation_type, ann| - case annotation_type - when @program.link - link_annotations ||= [] of LinkAnnotation - link_annotations << LinkAnnotation.from(ann) - when @program.call_convention - call_convention = parse_call_convention(ann, call_convention) - end - end - - @annotations = nil - - {link_annotations, call_convention} - end - def include_in(current_type, node, kind) node_name = node.name @@ -1015,8 +1013,18 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor node.add_hook_expansion(expansion) end - if kind == :inherited && (superclass = type_with_hooks.instance_type.superclass) - run_hooks(superclass.metaclass, current_type, kind, node) + if kind == :inherited + # In the case of: + # + # class A(X); end + # clsss B < A(Int32);end + # + # we need to go from A(Int32) to A(X) to go up the hierarchy. + if type_with_hooks.is_a?(GenericClassInstanceMetaclassType) + run_hooks(type_with_hooks.instance_type.generic_type.metaclass, current_type, kind, node) + elsif (superclass = type_with_hooks.instance_type.superclass) + run_hooks(superclass.metaclass, current_type, kind, node) + end end end @@ -1047,9 +1055,10 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor call_convention end - def attach_doc(type, node) + def attach_doc(type, node, annotations) if @program.wants_doc? type.doc ||= node.doc + type.doc ||= annotations_doc(annotations) if annotations end if node_location = node.location @@ -1057,7 +1066,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor end end - def check_ditto(node : Def | Assign | FunDef | Const, location : Location?) : Nil + def check_ditto(node : Def | Assign | FunDef | Const | Macro, location : Location?) : Nil return if !@program.wants_doc? stripped_doc = node.doc.try &.strip if stripped_doc == ":ditto:" @@ -1092,12 +1101,6 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor node.returns_twice = true when @program.raises_annotation node.raises = true - when @program.deprecated_annotation - # Check whether a DeprecatedAnnotation can be built. - # There is no need to store it, but enforcing - # arguments makes sense here. - DeprecatedAnnotation.from(ann) - yield annotation_type, ann else yield annotation_type, ann end @@ -1154,7 +1157,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor next_type = base_type.lookup_path_item(name, lookup_in_namespace: false, include_private: true, location: path.location) if next_type if next_type.is_a?(ASTNode) - path.raise "execpted #{name} to be a type" + path.raise "expected #{name} to be a type" end else base_type = check_type_is_type_container(base_type, path) diff --git a/src/compiler/crystal/semantic/type_declaration_processor.cr b/src/compiler/crystal/semantic/type_declaration_processor.cr index c2fddca266c0..97f636207065 100644 --- a/src/compiler/crystal/semantic/type_declaration_processor.cr +++ b/src/compiler/crystal/semantic/type_declaration_processor.cr @@ -105,7 +105,7 @@ struct Crystal::TypeDeclarationProcessor # they are initialized in at least one of the initialize methods. @non_nilable_instance_vars = {} of Type => Array(String) - # Nilable variables there were detected to not be initilized in an initialize, + # Nilable variables there were detected to not be initialized in an initialize, # but a superclass does initialize it. It's only an error if the explicit/guessed # type is not nilable itself. @nilable_instance_vars = {} of Type => Hash(String, InitializeInfo) @@ -266,6 +266,11 @@ struct Crystal::TypeDeclarationProcessor unless supervar.type.same?(type_decl.type) raise TypeException.new("instance variable '#{name}' of #{supervar.owner}, with #{owner} < #{supervar.owner}, is already declared as #{supervar.type} (trying to re-declare as #{type_decl.type})", type_decl.location) end + + # Reject annotations to existing instance var + type_decl.annotations.try &.each do |_, ann| + ann.raise "can't annotate #{name} in #{owner} because it was first defined in #{supervar.owner}" + end else declare_meta_type_var(owner.instance_vars, owner, name, type_decl, instance_var: true, check_nilable: !owner.module?) remove_error owner, name @@ -339,13 +344,13 @@ struct Crystal::TypeDeclarationProcessor # set from uninstantiated generic types return if owner.is_a?(GenericInstanceType) + # If a superclass already defines this variable we ignore + # the guessed type information for subclasses + supervar = owner.lookup_instance_var?(name) + return if supervar + case owner when NonGenericClassType - # If a superclass already defines this variable we ignore - # the guessed type information for subclasses - supervar = owner.lookup_instance_var?(name) - return if supervar - type = type_info.type if nilable_instance_var?(owner, name) type = Type.merge!(type, @program.nil) diff --git a/src/compiler/crystal/semantic/warnings.cr b/src/compiler/crystal/semantic/warnings.cr index 20039040ee47..48fa1b504ef8 100644 --- a/src/compiler/crystal/semantic/warnings.cr +++ b/src/compiler/crystal/semantic/warnings.cr @@ -1,5 +1,20 @@ module Crystal class Program + # Which kind of warnings wants to be detected. + property warnings : Warnings = Warnings::All + + # Paths to ignore for warnings detection. + property warnings_exclude : Array(String) = [] of String + + # Detected warning failures. + property warning_failures = [] of String + + # If `true` compiler will error if warnings are found. + property error_on_warnings : Bool = false + + @deprecated_methods_detected = Set(String).new + @deprecated_macros_detected = Set(String).new + def report_warning(node : ASTNode, message : String) return unless self.warnings.all? return if self.ignore_warning_due_to_location?(node.location) @@ -15,11 +30,161 @@ module Crystal message = String.build do |io| exception = SyntaxException.new message, location.line_number, location.column_number, location.filename exception.warning = true - exception.append_to_s(nil, io) + exception.append_to_s(io, nil) end end self.warning_failures << message end + + def ignore_warning_due_to_location?(location : Location?) + return false unless location + + filename = location.original_filename + return false unless filename + + @program.warnings_exclude.any? do |path| + filename.starts_with?(path) + end + end + + def check_call_to_deprecated_macro(a_macro : Macro, call : Call) + return unless self.warnings.all? + + if (ann = a_macro.annotation(self.deprecated_annotation)) && + (deprecated_annotation = DeprecatedAnnotation.from(ann)) + call_location = call.location.try(&.macro_location) || call.location + + return if self.ignore_warning_due_to_location?(call_location) + short_reference = a_macro.short_reference + warning_key = call_location.try { |l| "#{short_reference} #{l}" } + + # skip warning if the call site was already informed + # if there is no location information just inform it. + return if !warning_key || @deprecated_macros_detected.includes?(warning_key) + @deprecated_macros_detected.add(warning_key) if warning_key + + message = deprecated_annotation.message + message = message ? " #{message}" : "" + + full_message = call.warning "Deprecated #{short_reference}.#{message}" + + self.warning_failures << full_message + end + end + + def check_call_to_deprecated_method(node : Call) + return unless @warnings.all? + + node.target_defs.try &.each do |target_def| + if (ann = target_def.annotation(deprecated_annotation)) && + (deprecated_annotation = DeprecatedAnnotation.from(ann)) + return if compiler_expanded_call(node) + return if ignore_warning_due_to_location?(node.location) + short_reference = target_def.short_reference + warning_key = node.location.try { |l| "#{short_reference} #{l}" } + + # skip warning if the call site was already informed + # if there is no location information just inform it. + return if !warning_key || @deprecated_methods_detected.includes?(warning_key) + @deprecated_methods_detected.add(warning_key) if warning_key + + message = deprecated_annotation.message + message = message ? " #{message}" : "" + + full_message = node.warning "Deprecated #{short_reference}.#{message}" + + self.warning_failures << full_message + end + end + end + + private def compiler_expanded_call(node : Call) + # Compiler generates a `_.initialize` call in `new` + node.obj.as?(Var).try { |v| v.name == "_" } && node.name == "initialize" + end + end + + class Macro + def short_reference + case owner + when Program + "top-level #{name}" + when MetaclassType + "#{owner.instance_type.to_s(generic_args: false)}.#{name}" + else + "#{owner}.#{name}" + end + end + end + + struct DeprecatedAnnotation + getter message : String? + + def initialize(@message = nil) + end + + def self.from(ann : Annotation) + args = ann.args + named_args = ann.named_args + + if named_args + ann.raise "too many named arguments (given #{named_args.size}, expected maximum 0)" + end + + message = nil + count = 0 + + args.each do |arg| + case count + when 0 + arg.raise "first argument must be a String" unless arg.is_a?(StringLiteral) + message = arg.value + else + ann.wrong_number_of "deprecated annotation arguments", args.size, "1" + end + + count += 1 + end + + new(message) + end + end + + class Def + def short_reference + case owner + when Program + "top-level #{name}" + when .metaclass? + "#{owner.instance_type}.#{name}" + else + "#{owner}##{name}" + end + end + end + + class Command + def report_warnings + compiler = @compiler + return unless compiler + + program = compiler.program + return if program.warning_failures.empty? + + program.warning_failures.each do |message| + STDERR.puts message + STDERR.puts "\n" + end + STDERR.puts "A total of #{program.warning_failures.size} warnings were found." + end + + def warnings_fail_on_exit? + compiler = @compiler + return false unless compiler + + program = compiler.program + program.error_on_warnings && program.warning_failures.size > 0 + end end end diff --git a/src/compiler/crystal/syntax/ast.cr b/src/compiler/crystal/syntax/ast.cr index d22d9bbb2954..7b1b489ad9c7 100644 --- a/src/compiler/crystal/syntax/ast.cr +++ b/src/compiler/crystal/syntax/ast.cr @@ -296,7 +296,11 @@ module Crystal class StringInterpolation < ASTNode property expressions : Array(ASTNode) - def initialize(@expressions : Array(ASTNode)) + # Removed indentation size. + # This property is only available when this is created from heredoc. + property heredoc_indent : Int32 + + def initialize(@expressions : Array(ASTNode), @heredoc_indent = 0) end def accept_children(visitor) @@ -1178,8 +1182,9 @@ module Crystal class When < ASTNode property conds : Array(ASTNode) property body : ASTNode + property? exhaustive : Bool - def initialize(@conds, body = nil) + def initialize(@conds : Array(ASTNode), body : ASTNode? = nil, @exhaustive = false) @body = Expressions.from body end @@ -1189,18 +1194,22 @@ module Crystal end def clone_without_location - When.new(@conds.clone, @body.clone) + When.new(@conds.clone, @body.clone, @exhaustive) end - def_equals_and_hash @conds, @body + def_equals_and_hash @conds, @body, @exhaustive end class Case < ASTNode property cond : ASTNode? property whens : Array(When) property else : ASTNode? + property? exhaustive : Bool - def initialize(@cond, @whens, @else = nil) + def initialize(@cond : ASTNode?, @whens : Array(When), @else : ASTNode?, @exhaustive : Bool) + @whens.each do |wh| + wh.exhaustive = self.exhaustive? + end end def accept_children(visitor) @@ -1210,10 +1219,10 @@ module Crystal end def clone_without_location - Case.new(@cond.clone, @whens.clone, @else.clone) + Case.new(@cond.clone, @whens.clone, @else.clone, @exhaustive) end - def_equals_and_hash @cond, @whens, @else + def_equals_and_hash @exhaustive, @cond, @whens, @else end class Select < ASTNode @@ -1264,7 +1273,6 @@ module Crystal class Path < ASTNode property names : Array(String) property? global : Bool - property name_size = 0 property visibility = Visibility::Public def initialize(@names : Array, @global = false) @@ -1278,6 +1286,10 @@ module Crystal new names, true end + def name_size + names.sum(&.size) + (names.size + (global? ? 0 : -1)) * 2 + end + # Returns true if this path has a single component # with the given name def single?(name) @@ -1286,7 +1298,6 @@ module Crystal def clone_without_location ident = Path.new(@names.clone, @global) - ident.name_size = name_size ident end @@ -2183,7 +2194,7 @@ module Crystal end def self.expand_line(location) - (location.try(&.original_location) || location).try(&.line_number) || 0 + (location.try(&.expanded_location) || location).try(&.line_number) || 0 end def self.expand_file_node(location) diff --git a/src/compiler/crystal/syntax/exception.cr b/src/compiler/crystal/syntax/exception.cr index 007b77adef8d..9b66a0fd209c 100644 --- a/src/compiler/crystal/syntax/exception.cr +++ b/src/compiler/crystal/syntax/exception.cr @@ -31,7 +31,7 @@ module Crystal end end - def append_to_s(source, io) + def append_to_s(io : IO, source) msg = @message.to_s error_message_lines = msg.lines default_message = "syntax error in #{@filename}:#{@line_number}" @@ -42,8 +42,8 @@ module Crystal io << remaining error_message_lines end - def to_s_with_source(source, io) - append_to_s source, io + def to_s_with_source(io : IO, source) + append_to_s io, source end def deepest_error_message diff --git a/src/compiler/crystal/syntax/lexer.cr b/src/compiler/crystal/syntax/lexer.cr index 40e33127d6c2..0c26ff6af905 100644 --- a/src/compiler/crystal/syntax/lexer.cr +++ b/src/compiler/crystal/syntax/lexer.cr @@ -14,6 +14,7 @@ module Crystal getter token : Token property line_number : Int32 property column_number : Int32 + property wants_symbol : Bool @filename : String | VirtualFile | Nil @stacked_filename : String | VirtualFile | Nil @token_end_location : Location? @@ -202,7 +203,6 @@ module Crystal when '=' next_char :"<<=" when '-' - here = IO::Memory.new(20) has_single_quote = false found_closing_single_quote = false @@ -2565,7 +2565,14 @@ module Crystal (char == 'o' && next_char == 'd' && next_char == 'u' && next_char == 'l' && next_char == 'e' && peek_not_ident_part_or_end_next_char && :module) ) when 's' - next_char == 't' && next_char == 'r' && next_char == 'u' && next_char == 'c' && next_char == 't' && !ident_part_or_end?(peek_next_char) && next_char && :struct + case next_char + when 'e' + next_char == 'l' && next_char == 'e' && next_char == 'c' && next_char == 't' && !ident_part_or_end?(peek_next_char) && next_char && :select + when 't' + next_char == 'r' && next_char == 'u' && next_char == 'c' && next_char == 't' && !ident_part_or_end?(peek_next_char) && next_char && :struct + else + false + end when 'u' next_char == 'n' && (char = next_char) && ( (char == 'i' && next_char == 'o' && next_char == 'n' && peek_not_ident_part_or_end_next_char && :union) || diff --git a/src/compiler/crystal/syntax/location.cr b/src/compiler/crystal/syntax/location.cr index 86ef22717bd4..00e8ca62cb64 100644 --- a/src/compiler/crystal/syntax/location.cr +++ b/src/compiler/crystal/syntax/location.cr @@ -18,20 +18,33 @@ class Crystal::Location # Returns the Location whose filename is a String, not a VirtualFile, # traversing virtual file expanded locations. - def original_location + def expanded_location case filename = @filename when String self when VirtualFile - filename.expanded_location.try &.original_location + filename.expanded_location.try &.expanded_location else nil end end - # Returns the filename of the `original_location` + # Returns the Location whose filename is a String, not a VirtualFile, + # traversing virtual file expanded locations leading to the original user source code + def macro_location + case filename = @filename + when String + self + when VirtualFile + filename.macro.location.try(&.macro_location) + else + nil + end + end + + # Returns the filename of the `expanded_location` def original_filename - original_location.try &.filename.as?(String) + expanded_location.try &.filename.as?(String) end def between?(min, max) diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr index fe40c93985b7..5ddd2d007be9 100644 --- a/src/compiler/crystal/syntax/parser.cr +++ b/src/compiler/crystal/syntax/parser.cr @@ -386,7 +386,7 @@ module Crystal ) push_var atomic next_token_skip_space - type = parse_single_type + type = parse_bare_proc_type atomic = UninitializedVar.new(atomic, type).at(location) return atomic else @@ -504,7 +504,7 @@ module Crystal RangeLiteral.new(exp, right, exclusive).at(location).at_end(right) end - macro parse_operator(name, next_operator, node, operators) + macro parse_operator(name, next_operator, node, operators, right_associative = false) def parse_{{name.id}} location = @token.location @@ -521,7 +521,7 @@ module Crystal slash_is_regex! next_token_skip_space_or_newline - right = parse_{{next_operator.id}} + right = parse_{{(right_associative ? name : next_operator).id}} left = ({{node.id}}).at(location).at_end(right) left.name_location = name_location if left.is_a?(Call) else @@ -579,7 +579,7 @@ module Crystal end parse_operator :mul_or_div, :pow, "Call.new left, method, right", %(:"*", :"/", :"//", :"%", :"&*") - parse_operator :pow, :prefix, "Call.new left, method, right", %(:"**", :"&**") + parse_operator :pow, :prefix, "Call.new left, method, right", %(:"**", :"&**"), right_associative: true def parse_prefix name_location = @token.location @@ -835,12 +835,12 @@ module Crystal if @token.type == :"(" next_token_skip_space_or_newline - type = parse_single_type + type = parse_bare_proc_type skip_space_or_newline check :")" next_token_skip_space else - type = parse_single_type + type = parse_union_type end IsA.new(atomic, type) @@ -851,13 +851,13 @@ module Crystal if @token.type == :"(" next_token_skip_space_or_newline - type = parse_single_type + type = parse_bare_proc_type skip_space_or_newline check :")" end_location = token_end_location next_token_skip_space else - type = parse_single_type(allow_commas: false) + type = parse_union_type end_location = token_end_location end @@ -942,7 +942,7 @@ module Crystal when :"{%" parse_percent_macro_control when :"::" - parse_ident_or_global_call + parse_generic_or_global_call when :"->" parse_fun_literal when :"@[" @@ -1156,7 +1156,7 @@ module Crystal set_visibility parse_var_or_call end when :CONST - parse_ident_or_literal + parse_generic_or_custom_literal when :INSTANCE_VAR if @in_macro_expression && @token.value == "@type" @is_macro_def = true @@ -1186,7 +1186,7 @@ module Crystal def parse_type_declaration(var) next_token_skip_space_or_newline - var_type = parse_single_type(allow_splat: true) + var_type = parse_bare_proc_type skip_space if @token.type == :"=" next_token_skip_space_or_newline @@ -1230,12 +1230,12 @@ module Crystal end end - def parse_ident_or_literal - ident = parse_ident - parse_custom_literal ident + def parse_generic_or_custom_literal + type = parse_generic(expression: true) + parse_custom_literal type end - def parse_custom_literal(ident) + def parse_custom_literal(type) skip_space if @token.type == :"{" @@ -1249,16 +1249,17 @@ module Crystal case tuple_or_hash when TupleLiteral - ary = ArrayLiteral.new(tuple_or_hash.elements, name: ident).at(tuple_or_hash.location) + ary = ArrayLiteral.new(tuple_or_hash.elements, name: type).at(tuple_or_hash.location) return ary when HashLiteral - tuple_or_hash.name = ident + tuple_or_hash.name = type return tuple_or_hash else raise "BUG: tuple_or_hash should be tuple or hash, not #{tuple_or_hash}" end end - ident + + type end def check_not_inside_def(message) @@ -1433,7 +1434,7 @@ module Crystal def parse_rescue_types types = [] of ASTNode while true - types << parse_ident + types << parse_generic skip_space if @token.type == :"|" next_token_skip_space @@ -1623,7 +1624,7 @@ module Crystal superclass = Self.new.at(@token.location) next_token else - superclass = parse_ident + superclass = parse_generic end end skip_statement_end @@ -1858,7 +1859,7 @@ module Crystal if @token.type == :":" next_token_skip_space_or_newline - type = parse_single_type + type = parse_bare_proc_type end if @token.type == :"," @@ -1900,7 +1901,19 @@ module Crystal end end when :CONST - obj = parse_ident + obj = parse_generic + check :"." + name = consume_def_or_macro_name + next_token_skip_space + when :INSTANCE_VAR + obj = InstanceVar.new(@token.value.to_s) + next_token_skip_space + check :"." + name = consume_def_or_macro_name + next_token_skip_space + when :CLASS_VAR + obj = ClassVar.new(@token.value.to_s) + next_token_skip_space check :"." name = consume_def_or_macro_name next_token_skip_space @@ -1914,7 +1927,7 @@ module Crystal if @token.type == :"(" next_token_skip_space - types = parse_types + types = parse_union_types(:")") check :")" next_token_skip_space else @@ -2126,11 +2139,13 @@ module Crystal node.expressions.push(StringLiteral.new(string).at(node.location).at_end(token_end_location)) end + node.heredoc_indent = delimiter_state.heredoc_indent + node.end_location = token_end_location end def needs_heredoc_indent_removed?(delimiter_state) - delimiter_state.kind == :heredoc && delimiter_state.heredoc_indent > 0 + delimiter_state.kind == :heredoc && delimiter_state.heredoc_indent >= 0 end def remove_heredoc_indent(pieces : Array, indent) @@ -2278,7 +2293,7 @@ module Crystal next_token_skip_space if @token.keyword?(:of) next_token_skip_space_or_newline - of = parse_single_type + of = parse_bare_proc_type ArrayLiteral.new(of: of).at_end(of) else raise "for empty arrays use '[] of ElementType'", line, column @@ -2317,7 +2332,7 @@ module Crystal of = nil if @token.keyword?(:of) next_token_skip_space_or_newline - of = parse_single_type + of = parse_bare_proc_type end_location = of.end_location elsif exps.size == 0 raise "for empty arrays use '[] of ElementType'", line, column @@ -2472,10 +2487,10 @@ module Crystal if allow_of if @token.keyword?(:of) next_token_skip_space_or_newline - of_key = parse_single_type + of_key = parse_bare_proc_type check :"=>" next_token_skip_space_or_newline - of_value = parse_single_type + of_value = parse_bare_proc_type of = HashLiteral::Entry.new(of_key, of_value) end_location = of_value.end_location end @@ -2578,6 +2593,7 @@ module Crystal whens = [] of When a_else = nil + exhaustive = nil # All when expressions, so we can detect duplicates when_exps = Set(ASTNode).new @@ -2586,7 +2602,18 @@ module Crystal case @token.type when :IDENT case @token.value - when :when + when :when, :in + if exhaustive.nil? + exhaustive = @token.value == :in + if exhaustive && !cond + raise "exhaustive case (case ... in) requires a case expression (case exp; in ..)" + end + elsif exhaustive && @token.value == :when + raise "expected 'in', not 'when'" + elsif !exhaustive && @token.value == :in + raise "expected 'when', not 'in'" + end + location = @token.location slash_is_regex! next_token_skip_space_or_newline @@ -2602,7 +2629,11 @@ module Crystal tuple_elements = [] of ASTNode while true - tuple_elements << parse_when_expression(cond, single: false) + exp = parse_when_expression(cond, single: false, exhaustive: exhaustive) + check_valid_exhaustive_expression(exp) if exhaustive + + tuple_elements << exp + skip_space if @token.type == :"," next_token_skip_space_or_newline @@ -2622,7 +2653,7 @@ module Crystal check :"}" next_token_skip_space else - exp = parse_when_expression(cond, single: true) + exp = parse_when_expression(cond, single: true, exhaustive: exhaustive) when_conds << exp add_when_exp(when_exps, exp) skip_space @@ -2632,7 +2663,9 @@ module Crystal end else while true - exp = parse_when_expression(cond, single: true) + exp = parse_when_expression(cond, single: true, exhaustive: exhaustive) + check_valid_exhaustive_expression(exp) if exhaustive + when_conds << exp add_when_exp(when_exps, exp) skip_space @@ -2644,6 +2677,10 @@ module Crystal skip_space_or_newline whens << When.new(when_conds, when_body).at(location) when :else + if exhaustive + raise "exhaustive case (case ... in) doesn't allow an 'else'" + end + next_token_skip_statement_end a_else = parse_expressions skip_statement_end @@ -2661,7 +2698,30 @@ module Crystal end end - Case.new(cond, whens, a_else) + Case.new(cond, whens, a_else, exhaustive.nil? ? false : exhaustive) + end + + def check_valid_exhaustive_expression(exp) + case exp + when NilLiteral, BoolLiteral, Path, Generic, Underscore + return + when Call + if exp.obj.is_a?(ImplicitObj) && exp.name.ends_with?('?') && + exp.args.empty? && !exp.named_args && + !exp.block + return + end + + if (exp.obj.is_a?(Path) || exp.obj.is_a?(Generic)) && exp.name == "class" && + exp.args.empty? && !exp.named_args && + !exp.block + return + end + else + # Go on + end + + raise "expression of exhaustive case (case ... in) must be a constant (like `IO::Memory`), a generic (like `Array(Int32)`) a bool literal (true or false), a nil literal (nil) or a question method (like `.red?`)", exp.location.not_nil! end # Adds an expression to all when expressions and error on duplicates @@ -2720,7 +2780,7 @@ module Crystal false end - def parse_when_expression(cond, single) + def parse_when_expression(cond, single, exhaustive) if cond && @token.type == :"." next_token call = parse_var_or_call(force_call: true) @@ -2736,7 +2796,11 @@ module Crystal end call elsif single && @token.type == :UNDERSCORE - raise "'when _' is not supported, use 'else' block instead" + if exhaustive + raise "'when _' is not supported" + else + raise "'when _' is not supported, use 'else' block instead" + end else parse_op_assign_no_control end @@ -2830,7 +2894,7 @@ module Crystal name.end_location = token_end_location next_token_skip_space else - name = parse_ident + name = parse_generic end klass.new name @@ -2992,7 +3056,7 @@ module Crystal next_macro_token macro_state, skip_whitespace macro_state = @token.macro_state if macro_state.yields - @yields = 0 + @yields ||= 0 end skip_whitespace = false @@ -3322,7 +3386,7 @@ module Crystal end_location = token_end_location if @token.type == :CONST - receiver = parse_ident(allow_type_vars: false) + receiver = parse_path elsif @token.type == :IDENT check_valid_def_name name = @token.value.to_s @@ -3352,7 +3416,8 @@ module Crystal raise "shouldn't reach this line" end end - next_token_skip_space + + consume_def_or_macro_name if @token.type == :IDENT check_valid_def_name @@ -3469,7 +3534,7 @@ module Crystal if @token.type == :":" next_token_skip_space - return_type = parse_single_type + return_type = parse_bare_proc_type end_location = return_type.end_location end @@ -3650,7 +3715,7 @@ module Crystal next_token end - restriction = parse_single_type(allow_splat: !splat_restriction) + restriction = parse_bare_proc_type if splat_restriction restriction = splat ? Splat.new(restriction) : DoubleSplat.new(restriction) @@ -3720,7 +3785,7 @@ module Crystal location = @token.location - type_spec = parse_single_type(allow_splat: true) + type_spec = parse_bare_proc_type end block_arg = Arg.new(arg_name, restriction: type_spec).at(name_location) @@ -3853,8 +3918,7 @@ module Crystal :extend, :class, :struct, :module, :enum, :while, :until, :return, :next, :break, :lib, :fun, :alias, :pointerof, :sizeof, :offsetof, :instance_sizeof, :typeof, :private, :protected, :asm, :out, - # `end` is also invalid because it maybe terminate `def` block. - :end + :self, :in, :end true else false @@ -3866,7 +3930,7 @@ module Crystal "extend", "class", "struct", "module", "enum", "while", "until", "return", "next", "break", "lib", "fun", "alias", "pointerof", "sizeof", "offsetof", "instance_sizeof", "typeof", "private", "protected", "asm", "out", - "end" + "self", "in", "end" true else false @@ -3992,6 +4056,16 @@ module Crystal is_var = var?(name) + # If the name is a var and '+' or '-' follow, never treat the name as a call + if is_var && next_comes_plus_or_minus? + var = Var.new(name) + var.doc = doc + var.location = name_location + var.end_location = name_location + next_token + return var + end + @wants_regex = false next_token @@ -4063,13 +4137,6 @@ module Crystal maybe_var = !force_call && is_var && !has_parentheses if maybe_var && args.size == 0 Var.new(name) - elsif maybe_var && args.size == 1 && (num = args[0]) && (num.is_a?(NumberLiteral) && num.has_sign?) - sign = num.value[0].to_s - num.value = num.value.byte_slice(1) - Call.new(Var.new(name), sign, args) - elsif maybe_var && args.size == 1 && (arg = args[0]) && arg.is_a?(Call) && !arg.obj.nil? && - arg.name.in?("+", "-") && (!arg.args || arg.args.size == 0) - Call.new(Var.new(name), arg.name, arg.obj.not_nil!) else call = Call.new(nil, name, args, nil, block_arg, named_args, global) call.name_location = name_location @@ -4104,6 +4171,16 @@ module Crystal node end + def next_comes_plus_or_minus? + pos = current_pos + while current_char.ascii_whitespace? + next_char_no_column_increment + end + comes_plus_or_minus = current_char == '+' || current_char == '-' + self.current_pos = pos + comes_plus_or_minus + end + def preserve_stop_on_do(new_value = false) old_stop_on_do = @stop_on_do @stop_on_do = new_value @@ -4165,7 +4242,12 @@ module Crystal case @token.type when :IDENT + if @token.keyword? && invalid_internal_name?(@token.value) + raise "cannot use '#{@token}' as a block argument name", @token + end + arg_name = @token.value.to_s + if all_names.includes?(arg_name) raise "duplicated block argument name: #{arg_name}", @token end @@ -4181,7 +4263,12 @@ module Crystal while true case @token.type when :IDENT + if @token.keyword? && invalid_internal_name?(@token.value) + raise "cannot use '#{@token}' as a block argument name", @token + end + sub_arg_name = @token.value.to_s + if all_names.includes?(sub_arg_name) raise "duplicated block argument name: #{sub_arg_name}", @token end @@ -4301,7 +4388,7 @@ module Crystal return parse_call_block_arg(args, true) end - if @token.type == :IDENT && current_char == ':' + if named_tuple_start? return parse_call_args_named_args(@token.location, args, first_name: nil, allow_newline: true) else arg = parse_call_arg(found_double_splat) @@ -4591,7 +4678,7 @@ module Crystal end end - def parse_ident_or_global_call + def parse_generic_or_global_call location = @token.location next_token_skip_space_or_newline @@ -4599,46 +4686,187 @@ module Crystal when :IDENT set_visibility parse_var_or_call global: true when :CONST - ident = parse_ident_after_colons location, - global: true, - allow_type_vars: true, - parse_nilable: true + ident = parse_generic global: true, location: location, expression: true parse_custom_literal ident else unexpected_token end end - def parse_ident(allow_type_vars = true, parse_nilable = true) + # Parse a **bare** proc type like `A, B, C -> D`. + # Generally it is entry point of type parsing and + # it is used on the context expected type (e.g. type restrictions, rhs of `alias` and more) + def parse_bare_proc_type + type = parse_type_splat { parse_union_type } + + # To determine to consume comma, looking-ahead is needed. + # Consider `[ [] of Int32, Foo.new ]`, we want to parse it as `[ ([] of Int32), Foo.new ]` of course. + # If the parser consumes comma afrer Int32 quickly, it may cause parsing error. + unless @token.type == :"->" || (@token.type == :"," && type_start?(consume_newlines: true)) + if type.is_a?(Splat) + raise "invalid type splat", type.location.not_nil! + end + return type + end + + input_types = [type] + if @token.type != :"->" + loop do + next_token_skip_space_or_newline + input_types << parse_type_splat { parse_union_type } + break unless @token.type == :"," && type_start?(consume_newlines: true) + end + end + + parse_proc_type_output(input_types, input_types.first.location) + end + + def parse_union_type + type = parse_atomic_type_with_suffix + return type unless @token.type == :| + + types = [type] + while @token.type == :| + next_token_skip_space_or_newline + types << parse_atomic_type_with_suffix + end + + Union.new(types).at(types.first).at_end(types.last) + end + + def parse_atomic_type_with_suffix + type = parse_atomic_type + parse_type_suffix type + end + + def parse_atomic_type location = @token.location - global = false + case @token.type + when :IDENT + case @token.value + when :self + next_token_skip_space + Self.new.at(location) + when "self?" + next_token_skip_space + make_nilable_type Self.new.at(location) + when :typeof + parse_typeof + else + unexpected_token + end + when :UNDERSCORE + next_token_skip_space + Underscore.new.at(location) + when :CONST, :"::" + parse_generic + when :"{" + next_token_skip_space_or_newline + if named_tuple_start? || @token.type == :DELIMITER_START + type = make_named_tuple_type parse_named_type_args(:"}") + else + type = make_tuple_type parse_union_types(:"}") + end + check :"}" + next_token_skip_space + type + when :"->" + parse_proc_type_output(nil, location) + when :"(" + next_token_skip_space_or_newline + type = parse_type_splat { parse_union_type } + if @token.type == :")" + next_token_skip_space + if @token.type == :"->" # `(A) -> B` case + type = parse_proc_type_output([type], location) + elsif type.is_a?(Splat) + raise "invalid type splat", type.location.not_nil! + end + else + input_types = [type] + while @token.type == :"," + next_token_skip_space_or_newline + break if @token.type == :")" # allow trailing comma + input_types << parse_type_splat { parse_union_type } + end + if @token.type == :"->" # `(A, B, C -> D)` case + type = parse_proc_type_output(input_types, input_types.first.location) + check :")" + next_token_skip_space + else # `(A, B, C) -> D` case + check :")" + next_token_skip_space + type = parse_proc_type_output(input_types, location) + end + end + type + else + unexpected_token + end + end - if @token.type == :UNDERSCORE - return node_and_next_token Underscore.new.at(location) + def parse_union_types(end_token) + types = [parse_union_type] + while @token.type == :"," + next_token_skip_space_or_newline + break if @token.type == end_token # allow trailing comma + types << parse_union_type end + types + end + # Parse generic type path like `A::B(C, D)?`. + # This method is used to parse not only a type, but also an expression represents type. + # And it also consumes prefix `::` to specify global path. + def parse_generic(expression = false) + location = @token.location + + global = false if @token.type == :"::" - global = true next_token_skip_space_or_newline + global = true end - check :CONST - parse_ident_after_colons(location, global, allow_type_vars, parse_nilable) + parse_generic global, location, expression end - def parse_path - name = parse_ident(allow_type_vars: false, parse_nilable: false) - raise "BUG: expected a Path" unless name.is_a?(Path) - name + def parse_generic(global, location, expression) + path = parse_path(global, location) + type = parse_type_args(path) + + # Nilable suffixes without any spaces are consumed here + # for expression represents nilable type. Typically such an expression + # is appeared in macro expression. (e.g. `{% if T <= Int32? %} ... {% end %}`) + # Note that the parser cannot consume any spaces because it conflicts ternary operator. + while expression && @token.type == :"?" + next_token + type = make_nilable_expression(type) + end + + skip_space + + type end - def parse_ident_after_colons(location, global, allow_type_vars, parse_nilable) - start_line = location.line_number - start_column = location.column_number + # Parse type path. + # It also consumes prefix `::` to specify global path. + def parse_path + location = @token.location + + global = false + if @token.type == :"::" + next_token_skip_space_or_newline + global = true + end + + path = parse_path(global, @token.location) + skip_space + path + end - names = [] of String - names << @token.value.to_s + def parse_path(global, location) + names = [check_const] end_location = token_end_location @wants_regex = false @@ -4647,58 +4875,44 @@ module Crystal next_token_skip_space_or_newline names << check_const end_location = token_end_location + @wants_regex = false next_token end - const = Path.new(names, global).at(location) - const.end_location = end_location - - token_location = @token.location - if token_location && token_location.line_number == start_line - const.name_size = token_location.column_number - start_column - end + Path.new(names, global).at(location).at_end(end_location) + end - if allow_type_vars && @token.type == :"(" - next_token_skip_space_or_newline + def parse_type_args(name) + return name unless @token.type == :"(" - if named_tuple_start? || @token.type == :DELIMITER_START - types = [] of ASTNode - named_args = parse_type_named_args(:")") - else - types = parse_types allow_primitives: true, allow_splat: true - if types.empty? - raise "must specify at least one type var" - end - named_args = nil + next_token_skip_space_or_newline + args = [] of ASTNode + if named_tuple_start? || string_literal_start? + named_args = parse_named_type_args(:")") + else + args << parse_type_splat { parse_type_arg } + while @token.type == :"," + next_token_skip_space_or_newline + break if @token.type == :")" # allow trailing comma + args << parse_type_splat { parse_type_arg } end - next_token if @token.type == :"," - - skip_space_or_newline - check :")" - const = Generic.new(const, types, named_args).at(location) - const.end_location = token_end_location - - next_token - end - - if parse_nilable - while @token.type == :"?" - const = Generic.new(Path.global("Union").at(const), [ - const, Path.global("Nil").at(const), - ] of ASTNode) - const.question = true - next_token + has_int = args.any? { |arg| arg.is_a?(NumberLiteral) || arg.is_a?(SizeOf) || arg.is_a?(InstanceSizeOf) || arg.is_a?(OffsetOf) } + if @token.type == :"->" && !has_int + args = [parse_proc_type_output(args, args.first.location)] of ASTNode end end - skip_space + skip_space_or_newline + check :")" + end_location = token_end_location + next_token - const + Generic.new(name, args, named_args).at(name).at_end(end_location) end - def parse_type_named_args(end_token) + def parse_named_type_args(end_token) named_args = [] of NamedArgument while @token.type != end_token @@ -4718,7 +4932,7 @@ module Crystal check :":" next_token_skip_space_or_newline - type = parse_single_type(allow_commas: false) + type = parse_bare_proc_type skip_space named_args << NamedArgument.new(name, type) @@ -4735,346 +4949,215 @@ module Crystal named_args end - def parse_types(allow_primitives = false, allow_splat = false) - type = parse_type(allow_primitives: allow_primitives, allow_splat: allow_splat, inside_paren: true) - case type - when Array - type - when ASTNode - [type] of ASTNode - else - raise "BUG" - end - end - - def parse_single_type(allow_primitives = false, allow_commas = true, allow_splat = false) + def parse_type_splat location = @token.location - type = parse_type(allow_primitives: allow_primitives, allow_commas: allow_commas, allow_splat: allow_splat) - case type - when Array - raise "unexpected ',' in type (use parentheses to disambiguate)", location - when ASTNode - type - else - raise "BUG" - end - end - - def parse_type(allow_primitives, allow_commas = true, allow_splat = false, inside_paren = false) - location = @token.location - - if @token.type == :"->" - input_types = nil - else - input_types = parse_type_union(allow_primitives, allow_splat) - input_types = [input_types] unless input_types.is_a?(Array) - while allow_commas && @token.type == :"," && ( - (allow_primitives && next_comes_type_or_int) || - (!allow_primitives && next_comes_type) || - (inside_paren && next_comes_curly) - ) - next_token_skip_space_or_newline - if @token.type == :"->" - next_types = parse_type(false) - case next_types - when Array - input_types.concat next_types - when ASTNode - input_types << next_types - end - next - else - type_union = parse_type_union(allow_primitives, allow_splat) - if type_union.is_a?(Array) - input_types.concat type_union - else - input_types << type_union - end - end - end - end - - if @token.type == :"->" - next_token_skip_space - case @token.type - when :"=", :",", :")", :"}", :";", :NEWLINE - return_type = nil - else - type_union = parse_type_union(allow_primitives, allow_splat) - if type_union.is_a?(Array) - raise "can't return more than more type", location.line_number, location.column_number - else - return_type = type_union - end - end - ProcNotation.new(input_types, return_type).at(location) - else - input_types = input_types.not_nil! - if input_types.size == 1 - input_types.first - else - input_types - end - end - end - def parse_type_union(allow_primitives, allow_splat) - types = [] of ASTNode - parse_type_with_suffix(types, allow_primitives, allow_splat) - if @token.type == :"|" - while @token.type == :"|" - next_token_skip_space_or_newline - parse_type_with_suffix(types, allow_primitives, allow_splat) - end - - if types.size == 1 - types.first - else - Union.new(types).at(types.first.location) - end - elsif types.size == 1 - types.first - else - types - end - end - - def parse_type_with_suffix(types, allow_primitives, allow_splat) splat = false - if allow_splat && @token.type == :"*" + if @token.type == :"*" + next_token_skip_space_or_newline splat = true - next_token end - location = @token.location + type = yield + type = Splat.new(type).at(location) if splat + type + end - if @token.type == :IDENT && @token.value == "self?" - type = Self.new.at(location) - type = Union.new([type, Path.global("Nil")] of ASTNode).at(location) + def parse_type_arg + if @token.type == :NUMBER + num = NumberLiteral.new(@token.value.to_s, @token.number_kind).at(@token.location) next_token_skip_space - elsif @token.keyword?(:self) - type = Self.new.at(location) - next_token_skip_space - else - case @token.type - when :"{" - next_token_skip_space_or_newline - - if named_tuple_start? || @token.type == :DELIMITER_START - named_args = parse_type_named_args(:"}") - else - type = parse_type(allow_primitives) - end - - # Allow a trailing comma - if @token.type == :"," - next_token_skip_space_or_newline - end - - check :"}" - next_token_skip_space - - if named_args - type = make_named_tuple_type(named_args).at(location) - else - case type - when Array - type = make_tuple_type(type).at(location) - when ASTNode - type = make_tuple_type([type] of ASTNode).at(location) - else - raise "BUG" - end - end - when :"(" - next_token_skip_space_or_newline - type = parse_type(allow_primitives, allow_splat: allow_splat) - check :")" - next_token_skip_space - case type - when Array - types.concat type - return - when ASTNode - # skip - else - raise "BUG" - end - else - if allow_primitives - if @token.type == :NUMBER - num = NumberLiteral.new(@token.value.to_s, @token.number_kind).at(@token.location) - types << node_and_next_token(num) - skip_space - return types - end - end - - type = parse_simple_type - end + return num end - type = Splat.new(type).at(location) if splat - types << parse_type_suffix(type) - end - - def parse_simple_type case @token - when .keyword?(:typeof) - type = parse_typeof when .keyword?(:sizeof) - type = parse_sizeof + parse_sizeof when .keyword?(:instance_sizeof) - type = parse_instance_sizeof + parse_instance_sizeof when .keyword?(:offsetof) - type = parse_offsetof + parse_offsetof else - type = parse_ident(parse_nilable: false) + parse_union_type end - skip_space - type end def parse_type_suffix(type) - while true + loop do case @token.type + when :"." + next_token_skip_space_or_newline + check_ident :class + next_token_skip_space + type = Metaclass.new(type).at(type) when :"?" - type = Union.new([type, Path.global("Nil")] of ASTNode).at(type.location) next_token_skip_space + type = make_nilable_type(type) when :"*" - type = make_pointer_type(type).at(type.location) next_token_skip_space + type = make_pointer_type(type) when :"**" - type = make_pointer_type(make_pointer_type(type)).at(type.location) next_token_skip_space + type = make_pointer_type(make_pointer_type(type)) when :"[" - next_token_skip_space - size = parse_single_type allow_primitives: true + next_token_skip_space_or_newline + size = parse_type_arg + skip_space_or_newline check :"]" - @wants_regex = false - next_token_skip_space - type = make_static_array_type(type, size).at(type.location) - when :"." - next_token - check_ident :class - type = Metaclass.new(type).at(type.location) next_token_skip_space + type = make_static_array_type(type, size) else - break + return type end end - type end - def parse_typeof - location = @token.location + def parse_proc_type_output(input_types, location) + has_output_type = type_start?(consume_newlines: false) + check :"->" next_token_skip_space - check :"(" - next_token_skip_space_or_newline - if @token.type == :")" - raise "missing typeof argument" - end - exps = [] of ASTNode - while @token.type != :")" - exps << parse_op_assign - if @token.type == :"," - next_token_skip_space_or_newline - else - skip_space_or_newline - check :")" - end + if has_output_type + skip_space_or_newline + output_type = parse_union_type end - end_location = token_end_location - next_token_skip_space + ProcNotation.new(input_types, output_type).at(location) + end - TypeOf.new(exps).at(location).at_end(end_location) + def make_nilable_type(type) + Union.new([type, Path.global("Nil").at(type)]).at(type) end - def next_comes_type - next_comes_type_or_int allow_int: false + def make_nilable_expression(type) + type = Generic.new(Path.global("Union").at(type), [type, Path.global("Nil").at(type)]).at(type) + type.question = true + type end - def next_comes_type_or_int(allow_int = true) - old_pos, old_line, old_column = current_pos, @line_number, @column_number + def make_pointer_type(type) + Generic.new(Path.global("Pointer").at(type), [type] of ASTNode).at(type) + end - @temp_token.copy_from(@token) + def make_static_array_type(type, size) + Generic.new(Path.global("StaticArray").at(type), [type, size] of ASTNode).at(type) + end - next_token_skip_space_or_newline + def make_tuple_type(types) + Generic.new(Path.global("Tuple"), types) + end - while @token.type == :"{" || @token.type == :"(" - next_token_skip_space_or_newline - end + def make_named_tuple_type(named_args) + Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: named_args) + end + + # Looks ahead next tokens to check whether they indicate type. + def type_start?(*, consume_newlines) + old_pos, old_line, old_column = current_pos, @line_number, @column_number + @temp_token.copy_from(@token) begin - case @token.type - when :CONST - next_token_skip_space - if @token.type == :"." - next_token_skip_space - @token.keyword?(:class) - else - true - end - when :UNDERSCORE - true - when :"->" - true - when :"*" - next_token - @token.type == :CONST - when :NUMBER - allow_int && @token.number_kind == :i32 - when :IDENT - case @token.value - when :typeof, :self, :sizeof, :instance_sizeof, :offsetof - true - else - false - end - when :"::" - next_token_skip_space - @token.type == :CONST + if consume_newlines + next_token_skip_space_or_newline else - false + next_token_skip_space end + + type_start? ensure @token.copy_from(@temp_token) self.current_pos, @line_number, @column_number = old_pos, old_line, old_column end end - def next_comes_curly - old_pos, old_line, old_column = current_pos, @line_number, @column_number - - @temp_token.copy_from(@token) + def type_start? + while @token.type == :"(" || @token.type == :"{" + next_token_skip_space_or_newline + end - next_token_skip_space_or_newline + # TODO: the below conditions are not complete, and there are many false-positive or true-negative examples. - curly = @token.type == :"{" + case @token.type + when :IDENT + return false if named_tuple_start? + case @token.value + when :typeof + true + when :self, "self?" + next_token_skip_space + delimiter_or_type_suffix? + else + false + end + when :CONST + return false if named_tuple_start? + type_path_start? + when :"::" + next_token + type_path_start? + when :UNDERSCORE, :"->" + true + when :"*" + next_token_skip_space_or_newline + type_start? + else + false + end + end - @token.copy_from(@temp_token) - self.current_pos, @line_number, @column_number = old_pos, old_line, old_column + def type_path_start? + while @token.type == :CONST + next_token + break unless @token.type == :"::" + next_token_skip_space_or_newline + end - curly + skip_space + delimiter_or_type_suffix? end - def make_pointer_type(node) - Generic.new(Path.global("Pointer").at(node), [node] of ASTNode).at(node) + def delimiter_or_type_suffix? + case @token.type + when :"." + next_token_skip_space_or_newline + @token.keyword?(:class) + when :"?", :"*", :"**" + # They are conflicted with operators, so more look-ahead is needed. + next_token_skip_space + delimiter_or_type_suffix? + when :"->", :"|", :",", :"=>", :NEWLINE, :EOF, :"=", :";", :"(", :")", :"[", :"]" + true + else + false + end end - def make_static_array_type(type, size) - Generic.new(Path.global("StaticArray").at(type), [type, size] of ASTNode).at(type.location).at(type) - end + def parse_typeof + location = @token.location - def make_tuple_type(types) - Generic.new(Path.global("Tuple"), types) - end + next_token_skip_space + check :"(" + next_token_skip_space_or_newline + if @token.type == :")" + raise "missing typeof argument" + end - def make_named_tuple_type(named_args) - Generic.new(Path.global("NamedTuple"), [] of ASTNode, named_args: named_args) + exps = [] of ASTNode + while @token.type != :")" + exps << parse_op_assign + if @token.type == :"," + next_token_skip_space_or_newline + else + skip_space_or_newline + check :")" + end + end + + end_location = token_end_location + next_token_skip_space + + TypeOf.new(exps).at(location).at_end(end_location) end def parse_visibility_modifier(modifier) @@ -5331,7 +5414,7 @@ module Crystal unexpected_token end when :CONST - ident = parse_ident + ident = parse_path(global: false, location: @token.location) skip_space check :"=" next_token_skip_space_or_newline @@ -5350,7 +5433,7 @@ module Crystal end check :":" next_token_skip_space_or_newline - type = parse_single_type + type = parse_bare_proc_type if name[0].ascii_uppercase? raise "external variables must start with lowercase, use for example `$#{name.underscore} = #{name} : #{type}`", location @@ -5422,17 +5505,15 @@ module Crystal next_token_skip_space_or_newline check :":" next_token_skip_space_or_newline - arg_type = parse_single_type + arg_type = parse_bare_proc_type skip_space_or_newline args << Arg.new(arg_name, nil, arg_type).at(arg_location) push_var_name arg_name if require_body else - arg_types = parse_types - arg_types.each do |arg_type_2| - args << Arg.new("", nil, arg_type_2).at(arg_type_2.location) - end + arg_type = parse_union_type + args << Arg.new("", nil, arg_type).at(arg_type.location) end if @token.type == :"," @@ -5448,7 +5529,7 @@ module Crystal if @token.type == :":" next_token_skip_space_or_newline - return_type = parse_single_type + return_type = parse_bare_proc_type end skip_statement_end @@ -5489,7 +5570,7 @@ module Crystal check :"=" next_token_skip_space_or_newline - value = parse_single_type + value = parse_bare_proc_type skip_space alias_node = Alias.new(name, value) @@ -5532,7 +5613,7 @@ module Crystal next_token_skip_space_or_newline location = @token.location - exp = parse_single_type.at(location) + exp = parse_bare_proc_type.at(location) skip_space_or_newline @@ -5549,7 +5630,7 @@ module Crystal next_token_skip_space_or_newline type_location = @token.location - type = parse_single_type.at(type_location) + type = parse_bare_proc_type.at(type_location) skip_space check :"," @@ -5577,7 +5658,7 @@ module Crystal check :"=" next_token_skip_space_or_newline - type = parse_single_type + type = parse_bare_proc_type skip_space typedef = TypeDef.new name, type @@ -5652,7 +5733,7 @@ module Crystal check :":" next_token_skip_space_or_newline - type = parse_single_type + type = parse_bare_proc_type skip_statement_end @@ -5673,7 +5754,7 @@ module Crystal case @token.type when :":" next_token_skip_space_or_newline - base_type = parse_single_type + base_type = parse_bare_proc_type skip_statement_end when :";", :NEWLINE skip_statement_end @@ -5794,7 +5875,7 @@ module Crystal true when :IDENT case @token.value - when :do, :end, :else, :elsif, :when, :rescue, :ensure, :then + when :do, :end, :else, :elsif, :when, :in, :rescue, :ensure, :then !next_comes_colon_space? else false diff --git a/src/compiler/crystal/syntax/to_s.cr b/src/compiler/crystal/syntax/to_s.cr index 94d514a6a3b9..4624393303e7 100644 --- a/src/compiler/crystal/syntax/to_s.cr +++ b/src/compiler/crystal/syntax/to_s.cr @@ -125,7 +125,7 @@ module Crystal @str << '[' end - node.elements.join(", ", @str, &.accept self) + node.elements.join(@str, ", ", &.accept self) if name @str << '}' @@ -177,7 +177,7 @@ module Crystal def visit(node : NamedTupleLiteral) @str << '{' - node.entries.join(", ", @str) do |entry| + node.entries.join(@str, ", ") do |entry| visit_named_arg_name(entry.key) @str << ": " entry.value.accept self @@ -396,7 +396,7 @@ module Crystal if node.name.ends_with?('=') && node.name[0].ascii_letter? @str << decorate_call(node, node.name.rchop) @str << " = " - node.args.join(", ", @str, &.accept self) + node.args.join(@str, ", ", &.accept self) else @str << decorate_call(node, node.name) @@ -592,9 +592,9 @@ module Crystal end def visit(node : MultiAssign) - node.targets.join(", ", @str, &.accept self) + node.targets.join(@str, ", ", &.accept self) @str << " = " - node.values.join(", ", @str, &.accept self) + node.values.join(@str, ", ", &.accept self) false end @@ -631,7 +631,7 @@ module Crystal @str << "->" if node.def.args.size > 0 @str << '(' - node.def.args.join(", ", @str, &.accept self) + node.def.args.join(@str, ", ", &.accept self) @str << ')' end @str << ' ' @@ -653,7 +653,7 @@ module Crystal if node.args.size > 0 @str << '(' - node.args.join(", ", @str, &.accept self) + node.args.join(@str, ", ", &.accept self) @str << ')' end false @@ -698,7 +698,7 @@ module Crystal if free_vars = node.free_vars @str << " forall " - free_vars.join(", ", @str) + free_vars.join(@str, ", ") end newline @@ -779,7 +779,7 @@ module Crystal def visit(node : MacroFor) @str << "{% for " - node.vars.join(", ", @str, &.accept self) + node.vars.join(@str, ", ", &.accept self) @str << " in " node.exp.accept self @str << " %}" @@ -795,7 +795,7 @@ module Crystal @str << node.name if exps = node.exps @str << '{' - exps.join(", ", @str, &.accept self) + exps.join(@str, ", ", &.accept self) @str << '}' end false @@ -855,7 +855,7 @@ module Crystal def visit(node : ProcNotation) @str << '(' if inputs = node.inputs - inputs.join(", ", @str, &.accept self) + inputs.join(@str, ", ", &.accept self) @str << ' ' end @str << "-> " @@ -872,7 +872,7 @@ module Crystal def visit(node : Path) @str << "::" if node.global? - node.names.join("::", @str) + node.names.join(@str, "::") end def visit(node : Generic) @@ -902,7 +902,7 @@ module Crystal printed_arg = false @str << '(' - node.type_vars.join(", ", @str) do |var| + node.type_vars.join(@str, ", ") do |var| var.accept self printed_arg = true end @@ -947,7 +947,7 @@ module Crystal end def visit(node : Union) - node.types.join(" | ", @str, &.accept self) + node.types.join(@str, " | ", &.accept self) false end @@ -982,7 +982,7 @@ module Crystal @str << keyword("yield") if node.exps.size > 0 @str << ' ' - node.exps.join(", ", @str, &.accept self) + node.exps.join(@str, ", ", &.accept self) end false end @@ -1039,7 +1039,7 @@ module Crystal first = node.elements.first? space = first.is_a?(TupleLiteral) || first.is_a?(NamedTupleLiteral) || first.is_a?(HashLiteral) @str << ' ' if space - node.elements.join(", ", @str, &.accept self) + node.elements.join(@str, ", ", &.accept self) @str << ' ' if space @str << '}' false @@ -1167,7 +1167,7 @@ module Crystal end if node.args.size > 0 @str << '(' - node.args.join(", ", @str) do |arg| + node.args.join(@str, ", ") do |arg| if arg_name = arg.name @str << arg_name << " : " end @@ -1343,9 +1343,11 @@ module Crystal cond.accept self end newline + node.whens.each do |wh| wh.accept self end + if node_else = node.else append_indent @str << keyword("else") @@ -1359,9 +1361,9 @@ module Crystal def visit(node : When) append_indent - @str << keyword("when") + @str << keyword(node.exhaustive? ? "in" : "when") @str << ' ' - node.conds.join(", ", @str, &.accept self) + node.conds.join(@str, ", ", &.accept self) newline accept_with_indent node.body false @@ -1431,7 +1433,7 @@ module Crystal @str << " :" end @str << ' ' - types.join(" | ", @str, &.accept self) + types.join(@str, " | ", &.accept self) end newline accept_with_indent node.body @@ -1450,7 +1452,7 @@ module Crystal def visit(node : TypeOf) @str << keyword("typeof") @str << '(' - node.expressions.join(", ", @str, &.accept self) + node.expressions.join(@str, ", ", &.accept self) @str << ')' false end @@ -1461,7 +1463,7 @@ module Crystal if !node.args.empty? || node.named_args @str << '(' printed_arg = false - node.args.join(", ", @str) do |arg| + node.args.join(@str, ", ") do |arg| arg.accept self printed_arg = true end @@ -1490,19 +1492,19 @@ module Crystal @str << " :" if outputs = node.outputs @str << ' ' - outputs.join(", ", @str, &.accept self) + outputs.join(@str, ", ", &.accept self) @str << ' ' end @str << ':' if inputs = node.inputs @str << ' ' - inputs.join(", ", @str, &.accept self) + inputs.join(@str, ", ", &.accept self) @str << ' ' end @str << ":" if clobbers = node.clobbers @str << ' ' - clobbers.join(", ", @str, &.inspect @str) + clobbers.join(@str, ", ", &.inspect @str) @str << ' ' end @str << ":" @@ -1560,6 +1562,7 @@ module Crystal with_indent do node.accept self end + newline if node.keyword == :"(" end def accept_with_indent(node : Nop) diff --git a/src/compiler/crystal/tools/doc/generator.cr b/src/compiler/crystal/tools/doc/generator.cr index 0a6ec2050ee4..4b1f7a03a7c7 100644 --- a/src/compiler/crystal/tools/doc/generator.cr +++ b/src/compiler/crystal/tools/doc/generator.cr @@ -2,32 +2,20 @@ class Crystal::Doc::Generator getter program : Program @base_dir : String - @repository : String? = nil - getter repository_name = "" getter project_info # Adding a flag and associated css class will add support in parser FLAG_COLORS = { - "BUG" => "red", - "DEPRECATED" => "red", - "FIXME" => "yellow", - "NOTE" => "purple", - "OPTIMIZE" => "green", - "TODO" => "orange", + "BUG" => "red", + "DEPRECATED" => "red", + "EXPERIMENTAL" => "lime", + "FIXME" => "yellow", + "NOTE" => "purple", + "OPTIMIZE" => "green", + "TODO" => "orange", } FLAGS = FLAG_COLORS.keys - GIT_REMOTE_PATTERNS = { - /github\.com(?:\:|\/)(?(?:\w|-|_)+)\/(?(?:\w|-|_|\.)+?)(?:\.git)?\s/ => { - repository: "https://github.com/%{user}/%{repo}/blob/%{rev}", - repo_name: "github.com/%{user}/%{repo}", - }, - /gitlab\.com(?:\:|\/)(?(?:\w|-|_|\.)+)\/(?(?:\w|-|_|\.)+?)(?:\.git)?\s/ => { - repository: "https://gitlab.com/%{user}/%{repo}/blob/%{rev}", - repo_name: "gitlab.com/%{user}/%{repo}", - }, - } - def self.new(program : Program, included_dirs : Array(String)) new(program, included_dirs, ".", "html", nil, "1.0", "never", ProjectInfo.new("test", "0.0.0-test")) end @@ -39,8 +27,6 @@ class Crystal::Doc::Generator @project_info : ProjectInfo) @base_dir = Dir.current.chomp @types = {} of Crystal::Type => Doc::Type - @repo_name = "" - compute_repository end def run @@ -80,7 +66,7 @@ class Crystal::Doc::Generator def generate_docs_json(program_type, types) readme = read_readme - json = Main.new(readme, Type.new(self, @program), repository_name) + json = Main.new(readme, Type.new(self, @program), project_info) puts json end @@ -97,7 +83,7 @@ class Crystal::Doc::Generator File.write File.join(@output_dir, "index.html"), MainTemplate.new(body, types, project_info) - main_index = Main.new(raw_body, Type.new(self, @program), repository_name) + main_index = Main.new(raw_body, Type.new(self, @program), project_info) File.write File.join(@output_dir, "index.json"), main_index File.write File.join(@output_dir, "search-index.js"), main_index.to_jsonp end @@ -306,7 +292,7 @@ class Crystal::Doc::Generator def summary(obj : Type | Method | Macro | Constant) doc = obj.doc - return if !doc && !obj.annotations(@program.deprecated_annotation) + return if !doc && !has_doc_annotations?(obj) summary obj, doc || "" end @@ -325,11 +311,15 @@ class Crystal::Doc::Generator def doc(obj : Type | Method | Macro | Constant) doc = obj.doc - return if !doc && !obj.annotations(@program.deprecated_annotation) + return if !doc && !has_doc_annotations?(obj) doc obj, doc || "" end + def has_doc_annotations?(obj) + obj.annotations(@program.deprecated_annotation) || obj.annotations(@program.experimental_annotation) + end + def doc(context, string) string = isolate_flag_lines string string += build_flag_lines_from_annotations context @@ -360,7 +350,7 @@ class Crystal::Doc::Generator def isolate_flag_lines(string) flag_regexp = /^ ?(#{FLAGS.join('|')}):?/ String.build do |io| - string.each_line(chomp: false).join("", io) do |line, io| + string.each_line(chomp: false).join(io) do |line, io| if line =~ flag_regexp io << '\n' << line else @@ -380,115 +370,48 @@ class Crystal::Doc::Generator io << "DEPRECATED: #{DeprecatedAnnotation.from(ann).message}\n\n" end end - end - end - - def compute_repository - # check whether inside git work-tree - `git rev-parse --is-inside-work-tree >/dev/null 2>&1` - return unless $?.success? - - remotes = `git remote -v` - return unless $?.success? - - git_matches = remotes.each_line.compact_map do |line| - GIT_REMOTE_PATTERNS.each_key.compact_map(&.match(line)).first? - end.to_a - - origin = git_matches.find(&.string.starts_with?("origin")) || git_matches.first? - return unless origin - - user = origin["user"] - repo = origin["repo"] - rev = `git rev-parse HEAD`.chomp - info = GIT_REMOTE_PATTERNS[origin.regex] - @repository = info[:repository] % {user: user, repo: repo, rev: rev} - @repository_name = info[:repo_name] % {user: user, repo: repo} + if anns = context.annotations(@program.experimental_annotation) + anns.each do |ann| + io << "\n\n" if first + first = false + io << "EXPERIMENTAL: #{ExperimentalAnnotation.from(ann).message}\n\n" + end + end + end end def source_link(node) - location = relative_location node + location = RelativeLocation.from(node, @base_dir) return unless location - - filename = relative_filename location - return unless filename - - "#{@repository}#{filename}#L#{location.line_number}" - end - - def relative_location(node : ASTNode) - relative_location node.location - end - - def relative_location(location : Location?) - return unless location - - repository = @repository - return unless repository - - filename = location.filename - if filename.is_a?(VirtualFile) - location = filename.expanded_location - end - - location - end - - def relative_filename(location) - filename = location.filename - return unless filename.is_a?(String) - return unless filename.starts_with? @base_dir - filename[@base_dir.size..-1] - end - - class RelativeLocation - property show_line_number - getter filename, line_number, url - - def initialize(@filename : String, @line_number : Int32, @url : String?, @show_line_number : Bool) - end - - def to_json(builder : JSON::Builder) - builder.object do - builder.field "filename", filename - builder.field "line_number", line_number - builder.field "url", url - end - end + project_info.source_url(location) end SRC_SEP = "src#{File::SEPARATOR}" def relative_locations(type) - repository = @repository locations = [] of RelativeLocation type.locations.try &.each do |location| - location = relative_location location + location = RelativeLocation.from(location, @base_dir) next unless location - - filename = relative_filename location + filename = location.filename next unless filename - url = "#{repository}#{filename}" if repository - - filename = filename[1..-1] if filename.starts_with? File::SEPARATOR - filename = filename[4..-1] if filename.starts_with? SRC_SEP + url = project_info.source_url(location) + next unless url + location.url = url # Prevent identical link generation in the "Defined in:" section in the docs because of macros - next if locations.any? { |loc| loc.filename == filename && loc.line_number == location.line_number } + next if locations.includes?(location) - show_line_number = locations.any? do |location| - if location.filename == filename - location.show_line_number = true - true - else - false - end + same_file_location = locations.find { |loc| loc.filename == filename } + if same_file_location + location.show_line_number = true + same_file_location.show_line_number = true end - locations << RelativeLocation.new(filename, location.line_number, url, show_line_number) + locations << location end - locations + locations.sort end end diff --git a/src/compiler/crystal/tools/doc/highlighter.cr b/src/compiler/crystal/tools/doc/highlighter.cr index 640f5096f0e8..6d0e387b76c3 100644 --- a/src/compiler/crystal/tools/doc/highlighter.cr +++ b/src/compiler/crystal/tools/doc/highlighter.cr @@ -17,12 +17,24 @@ module Crystal::Doc::Highlighter private def highlight_normal_state(lexer, io, break_on_rcurly = false) last_is_def = false + heredoc_stack = [] of Token while true token = lexer.next_token case token.type when :NEWLINE io.puts + heredoc_stack.each_with_index do |token, i| + highlight_delimiter_state lexer, token, io, heredoc: true + unless i == heredoc_stack.size - 1 + # Next token to heredoc's end is either NEWLINE or EOF. + # We can't continue highlighting when it is EOF even though + # heredoc tokens still remain. + break if lexer.next_token.type == :EOF + io.puts + end + end + heredoc_stack.clear when :SPACE io << token.value when :COMMENT @@ -36,7 +48,12 @@ module Crystal::Doc::Highlighter when :CONST, :"::" highlight token, "t", io when :DELIMITER_START - highlight_delimiter_state lexer, token, io + if token.delimiter_state.kind == :heredoc + highlight HTML.escape(token.raw), "s", io + heredoc_stack << token.dup + else + highlight_delimiter_state lexer, token, io + end when :STRING_ARRAY_START, :SYMBOL_ARRAY_START highlight_string_array lexer, token, io when :EOF @@ -83,17 +100,16 @@ module Crystal::Doc::Highlighter end end - private def highlight_delimiter_state(lexer, token, io) + private def highlight_delimiter_state(lexer, token, io, heredoc = false) start_highlight_class "s", io - HTML.escape(token.raw, io) + HTML.escape(token.raw, io) unless heredoc while true token = lexer.next_string_token(token.delimiter_state) case token.type when :DELIMITER_END HTML.escape(token.raw, io) - end_highlight_class io break when :INTERPOLATION_START end_highlight_class io @@ -101,12 +117,12 @@ module Crystal::Doc::Highlighter highlight_normal_state lexer, io, break_on_rcurly: true highlight "}", "i", io start_highlight_class "s", io - when :EOF - break else HTML.escape(token.raw, io) end end + ensure # This ensure is necessary to handle unterminated string literal. + end_highlight_class io end private def highlight_string_array(lexer, token, io) diff --git a/src/compiler/crystal/tools/doc/html/css/style.css b/src/compiler/crystal/tools/doc/html/css/style.css index 72506d4ab21a..20bb0840335e 100644 --- a/src/compiler/crystal/tools/doc/html/css/style.css +++ b/src/compiler/crystal/tools/doc/html/css/style.css @@ -146,6 +146,7 @@ body { color: inherit; font-family: inherit; font-size: inherit; + line-height: inherit; } .project-versions-nav:focus { outline: none; @@ -459,6 +460,12 @@ span.flag.purple { border-color: #1F0B37; } +span.flag.lime { + background-color: #a3ff00; + color: #222222; + border-color: #00ff1e; +} + .tooltip>span { position: absolute; opacity: 0; diff --git a/src/compiler/crystal/tools/doc/html/js/_versions.js b/src/compiler/crystal/tools/doc/html/js/_versions.js index 6e36aa6b49cc..9d0a390c9028 100644 --- a/src/compiler/crystal/tools/doc/html/js/_versions.js +++ b/src/compiler/crystal/tools/doc/html/js/_versions.js @@ -30,7 +30,7 @@ CrystalDocs.loadConfig = function (config) { var currentVersion = document.querySelector("html > head > meta[name=\"crystal_docs.project_version\"]").getAttribute("content") var currentVersionInList = projectVersions.find(function (element) { - element.version == currentVersion + return element.name == currentVersion }) if (!currentVersionInList) { diff --git a/src/compiler/crystal/tools/doc/html/type.html b/src/compiler/crystal/tools/doc/html/type.html index 0834d7190c54..9c67e3e8ac39 100644 --- a/src/compiler/crystal/tools/doc/html/type.html +++ b/src/compiler/crystal/tools/doc/html/type.html @@ -54,12 +54,12 @@

Defined in:

<% locations.each do |location| %> - <% if url = location.url %> - - <%= location.filename %><% if location.show_line_number %>:<%= location.line_number %><% end %> + <% if url = project_info.source_url(location) %> + + <%= location.filename_in_project %><% if location.show_line_number %>:<%= location.line_number %><% end %> <% else %> - <%= location.filename %><% if location.show_line_number %>:<%= location.line_number %><% end %> + <%= location.filename_in_project %><% if location.show_line_number %>:<%= location.line_number %><% end %> <% end %>
<% end %> diff --git a/src/compiler/crystal/tools/doc/macro.cr b/src/compiler/crystal/tools/doc/macro.cr index f84eafd90db6..e7072d6e611d 100644 --- a/src/compiler/crystal/tools/doc/macro.cr +++ b/src/compiler/crystal/tools/doc/macro.cr @@ -145,7 +145,6 @@ class Crystal::Doc::Macro end def annotations(annotation_type) - # macros does not support annotations - nil + @macro.annotations(annotation_type) end end diff --git a/src/compiler/crystal/tools/doc/main.cr b/src/compiler/crystal/tools/doc/main.cr index cfae49d9da47..03fd0fa27a5f 100644 --- a/src/compiler/crystal/tools/doc/main.cr +++ b/src/compiler/crystal/tools/doc/main.cr @@ -1,5 +1,5 @@ module Crystal::Doc - record Main, body : String, program : Type, repository_name : String do + record Main, body : String, program : Type, project_info : ProjectInfo do def to_s(io : IO) : Nil to_json(io) end @@ -18,7 +18,7 @@ module Crystal::Doc def to_json(builder : JSON::Builder) builder.object do - builder.field "repository_name", repository_name + builder.field "repository_name", project_info.name builder.field "body", body builder.field "program", program end diff --git a/src/compiler/crystal/tools/doc/method.cr b/src/compiler/crystal/tools/doc/method.cr index 4cc1341e19dd..320f270799bd 100644 --- a/src/compiler/crystal/tools/doc/method.cr +++ b/src/compiler/crystal/tools/doc/method.cr @@ -38,7 +38,7 @@ class Crystal::Doc::Method end # Returns this method's docs ready to be shown (before formatting) - # in the UI. This includes copiying docs from previous def or + # in the UI. This includes copying docs from previous def or # ancestors and replacing `:inherit:` with the ancestor docs. # This docs not include the "Description copied from ..." banner # in case it's needed. @@ -262,7 +262,7 @@ class Crystal::Doc::Method if free_vars = @def.free_vars io << " forall " - free_vars.join(", ", io) + free_vars.join(io, ", ") end io diff --git a/src/compiler/crystal/tools/doc/project_info.cr b/src/compiler/crystal/tools/doc/project_info.cr index 3e9bd0e48243..01ec280e0076 100644 --- a/src/compiler/crystal/tools/doc/project_info.cr +++ b/src/compiler/crystal/tools/doc/project_info.cr @@ -3,11 +3,13 @@ module Crystal::Doc property! name : String property! version : String property json_config_url : String? = nil + property refname : String? = nil + property source_url_pattern : String? = nil - def initialize(@name : String? = nil, @version : String? = nil) + def initialize(@name : String? = nil, @version : String? = nil, @refname : String? = nil, @source_url_pattern : String? = nil) end - def_equals_and_hash @name, @version, @json_config_url + def_equals_and_hash @name, @version, @json_config_url, @refname, @source_url_pattern def crystal_stdlib? name == "Crystal" @@ -20,6 +22,16 @@ module Crystal::Doc end end + if ProjectInfo.git_clean? + self.refname ||= ProjectInfo.git_ref(branch: false) + end + + unless source_url_pattern + if remote = ProjectInfo.git_remote + self.source_url_pattern = ProjectInfo.find_source_url_pattern(remote) + end + end + unless name? && version? shard_name, shard_version = ProjectInfo.read_shard_properties if shard_name && !name? @@ -31,6 +43,16 @@ module Crystal::Doc end end + def source_url(location : RelativeLocation) + refname = self.refname + url_pattern = source_url_pattern + + return unless refname && url_pattern + + url = url_pattern % {refname: refname, path: location.filename, filename: File.basename(location.filename), line: location.line_number} + url.presence + end + def self.git_dir? Process.run("git", ["rev-parse", "--is-inside-work-tree"]).success? end @@ -38,7 +60,7 @@ module Crystal::Doc VERSION_TAG = /^v(\d+[-.][-.a-zA-Z\d]+)$/ def self.find_git_version - if ref = git_ref + if ref = git_ref(branch: true) if ref.matches?(VERSION_TAG) ref = ref.byte_slice(1) end @@ -51,6 +73,64 @@ module Crystal::Doc end end + def self.find_source_url_pattern(remote) + if (at_index = remote.index('@')) && (colon_index = remote.index(':')) && at_index < colon_index + # SSH URI + host = remote[(at_index + 1)...colon_index] + path = remote[(colon_index + 1)..] + else + begin + uri = URI.parse(remote) + rescue URI::Error + return + end + host = uri.host + path = uri.path + end + + path = path.strip("/") + + case host + when "github.com", "www.github.com" + # GitHub only resolves URLs with the canonical repo name without .git extension. + path = path.rchop(".git") + "https://github.com/#{path}/blob/%{refname}/%{path}#L%{line}" + when "gitlab.com", "www.gitlab.com" + # Gitlab only resolves URLs with the canonical repo name without .git extension. + path = path.rchop(".git") + "https://gitlab.com/#{path}/blob/%{refname}/%{path}#L%{line}" + when "bitbucket.com", "www.bitbucket.com" + # Bitbucket does resolve URLs the URL with .git extension, but without it + # the canonical form and should be preferred. + path = path.rchop(".git") + "https://bitbucket.com/#{path}/src/%{refname}/%{path}#%{filename}-%{line}" + when "git.sr.ht" + # On git.sr.ht ~foo/bar and ~foo/bar.git seem to mean different repos. + "https://git.sr.ht/#{path}/tree/%{refname}/%{path}#L%{line}" + else + # Unknown remote host, can't determine source url pattern + end + end + + def self.git_remote + # check whether inside git work-tree + status = Process.run("git", ["rev-parse", "--is-inside-work-tree"]) + return unless status.success? + + io = IO::Memory.new + status = Process.run("git", ["remote", "-v"], output: io) + return unless status.success? + + remotes = io.to_s.lines.select(&.ends_with?(" (fetch)")) + + git_remote = remotes.find(&.starts_with?("origin\t")) || remotes.first? || return + + start_pos = git_remote.index("\t") + end_pos = git_remote.rindex(" ") + return unless start_pos && end_pos + git_remote[(start_pos + 1)...end_pos].presence + end + def self.git_clean? # Use git to determine if index and working directory are clean io = IO::Memory.new @@ -62,7 +142,7 @@ module Crystal::Doc io.bytesize == 0 end - def self.git_ref + def self.git_ref(*, branch) io = IO::Memory.new # Check if current HEAD is tagged status = Process.run("git", ["tag", "--points-at", "HEAD"], output: io) @@ -73,13 +153,26 @@ module Crystal::Doc if tag = tags.first? return tag end - - # Otherwise, return current branch name io.clear - status = Process.run("git", ["rev-parse", "--abbrev-ref", "HEAD"], output: io) + + if branch + # Read current branch name + status = Process.run("git", ["rev-parse", "--abbrev-ref", "HEAD"], output: io) + return unless status.success? + + if branch_name = io.to_s.strip.presence + return branch_name + end + io.clear + end + + # Otherwise, return current commit sha + status = Process.run("git", ["rev-parse", "HEAD"], output: io) return unless status.success? - io.to_s.strip.presence + if sha = io.to_s.strip.presence + return sha + end end def self.read_shard_properties diff --git a/src/compiler/crystal/tools/doc/relative_location.cr b/src/compiler/crystal/tools/doc/relative_location.cr new file mode 100644 index 000000000000..32d43cc1e34c --- /dev/null +++ b/src/compiler/crystal/tools/doc/relative_location.cr @@ -0,0 +1,52 @@ +class Crystal::Doc::RelativeLocation + include Comparable(self) + property show_line_number : Bool = false + + # This property is only used to keep backwards compatibility in JSON output. + property url : String? + + getter filename, line_number + + def initialize(@filename : String, @line_number : Int32) + end + + def_equals_and_hash @filename, @line_number + + def filename_in_project + filename.lchop("src/") + end + + def to_json(builder : JSON::Builder) + builder.object do + builder.field "filename", filename + builder.field "line_number", line_number + builder.field "url", url + end + end + + def <=>(other : self) + cmp = filename <=> other.filename + return cmp unless cmp == 0 + line_number <=> other.line_number + end + + def self.from(node : ASTNode, base_dir : String) + if location = node.location + from(location, base_dir) + end + end + + def self.from(location : Location, base_dir : String) + filename = location.filename + if filename.is_a?(VirtualFile) + location = filename.expanded_location || return + filename = location.filename + end + + return unless filename.is_a?(String) + return unless filename.starts_with? base_dir + filename = filename[(base_dir.size + 1)..] + + new(filename, location.line_number) + end +end diff --git a/src/compiler/crystal/tools/doc/type.cr b/src/compiler/crystal/tools/doc/type.cr index faeac5d7a944..8acbc80753ad 100644 --- a/src/compiler/crystal/tools/doc/type.cr +++ b/src/compiler/crystal/tools/doc/type.cr @@ -496,26 +496,9 @@ class Crystal::Doc::Type end def node_to_html(node : Generic, io, links = true) - match = lookup_path(node.name.as(Path)) - if match - if match.must_be_included? - if links - io << %() - end - match.full_name_without_type_vars(io) - if links - io << "" - end - else - io << node.name - end - else - io << node.name - end + node_to_html node.name, io, links: links io << '(' - node.type_vars.join(", ", io) do |type_var| + node.type_vars.join(io, ", ") do |type_var| node_to_html type_var, io, links: links end io << ')' @@ -523,7 +506,7 @@ class Crystal::Doc::Type def node_to_html(node : ProcNotation, io, links = true) if inputs = node.inputs - inputs.join(", ", io) do |input| + inputs.join(io, ", ") do |input| node_to_html input, io, links: links end end @@ -544,7 +527,7 @@ class Crystal::Doc::Type end end - node.types.join(" | ", io) do |elem| + node.types.join(io, " | ") do |elem| node_to_html elem, io, links: links end end @@ -597,7 +580,7 @@ class Crystal::Doc::Type separator = " | " end - type.union_types.join(separator, io) do |union_type| + type.union_types.join(io, separator) do |union_type| type_to_html union_type, io, text, links: links end @@ -605,7 +588,7 @@ class Crystal::Doc::Type end def type_to_html(type : Crystal::ProcInstanceType, io, text = nil, links = true) - type.arg_types.join(", ", io) do |arg_type| + type.arg_types.join(io, ", ") do |arg_type| type_to_html arg_type, io, links: links end io << " -> " @@ -615,7 +598,7 @@ class Crystal::Doc::Type def type_to_html(type : Crystal::TupleInstanceType, io, text = nil, links = true) io << '{' - type.tuple_types.join(", ", io) do |tuple_type| + type.tuple_types.join(io, ", ") do |tuple_type| type_to_html tuple_type, io, links: links end io << '}' @@ -623,7 +606,7 @@ class Crystal::Doc::Type def type_to_html(type : Crystal::NamedTupleInstanceType, io, text = nil, links = true) io << '{' - type.entries.join(", ", io) do |entry| + type.entries.join(io, ", ") do |entry| if Symbol.needs_quotes?(entry.name) entry.name.inspect(io) else @@ -655,7 +638,7 @@ class Crystal::Doc::Type io << "" if must_be_included && links && has_link_in_type_vars io << '(' - type.type_vars.values.join(", ", io) do |type_var| + type.type_vars.values.join(io, ", ") do |type_var| case type_var when Var type_to_html type_var.type, io, links: links @@ -758,7 +741,7 @@ class Crystal::Doc::Type end def html_id - "#{@generator.repository_name}/" + ( + "#{@generator.project_info.name}/" + ( if program? "toplevel" elsif namespace = self.namespace @@ -786,7 +769,7 @@ class Crystal::Doc::Type end end builder.field "locations", locations - builder.field "repository_name", @generator.repository_name + builder.field "repository_name", @generator.project_info.name builder.field "program", program? builder.field "enum", enum? builder.field "alias", alias? diff --git a/src/compiler/crystal/tools/formatter.cr b/src/compiler/crystal/tools/formatter.cr index cf2bf4618f1d..1e3c9eec07c4 100644 --- a/src/compiler/crystal/tools/formatter.cr +++ b/src/compiler/crystal/tools/formatter.cr @@ -154,7 +154,7 @@ module Crystal def end_visit_any(node) case node - when StringLiteral, StringInterpolation + when StringInterpolation # Nothing else @last_is_heredoc = false @@ -220,9 +220,14 @@ module Crystal write "begin" @indent += 2 write_line - next_token_skip_space_or_newline - if @token.type == :";" - next_token_skip_space_or_newline + next_token + # Corner case: an empty `begin ... end`. + # In this case, we should not skip space because it will do in the below loop. + unless node.expressions.size == 1 && node.expressions[0].is_a?(Nop) + skip_space_or_newline + if @token.type == :";" + next_token_skip_space_or_newline + end end has_begin = true base_indent = @indent @@ -231,6 +236,7 @@ module Crystal end last_aligned_assign = nil + last_found_comment = false max_length = nil skip_space @@ -283,12 +289,18 @@ module Crystal end if last?(i, node.expressions) - skip_space_or_newline last: true + last_found_comment = skip_space_or_newline last: true, next_comes_end: true else if needs_two_lines unless found_comment - skip_space_write_line - found_comment = skip_space_or_newline last: true, at_least_one: true + if @wrote_newline + write_line unless @wrote_double_newlines + elsif !@wrote_double_newlines + write_line + write_line + end + @wrote_double_newlines = true + found_comment = skip_space_or_newline write_line unless found_comment || @wrote_double_newlines end else @@ -301,7 +313,7 @@ module Crystal @indent = old_indent - if has_newline + if has_newline && !last_found_comment write_line write_indent end @@ -447,8 +459,6 @@ module Crystal end def visit(node : StringLiteral) - @last_is_heredoc = false - column = @column if @token.type == :__FILE__ || @token.type == :__DIR__ @@ -459,12 +469,6 @@ module Crystal check :DELIMITER_START is_regex = @token.delimiter_state.kind == :regex - is_heredoc = @token.delimiter_state.kind == :heredoc - @last_is_heredoc = is_heredoc - - indent_difference = @token.column_number - (@column + 1) - heredoc_line = @line - heredoc_end = @line write @token.raw next_string_token @@ -488,7 +492,6 @@ module Crystal write "}" next_string_token when :DELIMITER_END - heredoc_end = @line break else raise "Bug: unexpected token: #{@token.type}" @@ -498,16 +501,6 @@ module Crystal write @token.raw format_regex_modifiers if is_regex - if is_heredoc - if indent_difference > 0 - @heredoc_fixes << HeredocFix.new(heredoc_line, @line, indent_difference) - end - (heredoc_line...heredoc_end).each do |line| - @no_rstrip_lines.add line - end - write_line - end - if space_slash_newline? old_indent = @indent @indent = column if @string_continuation == 0 @@ -556,6 +549,16 @@ module Crystal heredoc_line = @line heredoc_end = @line + # To detect the first content of interpolation of string literal correctly, + # we should consume the first string token if this token contains only removed indentation of heredoc. + if is_heredoc && @token.type == :STRING + token_is_indent = @token.raw.bytesize == node.heredoc_indent && @token.raw.each_char.all? &.ascii_whitespace? + if token_is_indent + write @token.raw + next_string_token + end + end + node.expressions.each do |exp| if @token.type == :DELIMITER_END # Heredoc cannot contain string continuation, @@ -588,17 +591,19 @@ module Crystal check :"}" write "}" @token.delimiter_state = delimiter_state + next_string_token else - if @token.invalid_escape - write @token.value - else - write @token.raw + loop do + check :STRING + write @token.invalid_escape ? @token.value : @token.raw + next_string_token + + # On heredoc, pieces of contents are combined due to removing indentation. + # Thus, we should consume continuous string tokens at once. + break if !is_heredoc || @token.type != :STRING end end - next_string_token else - skip_strings - check :INTERPOLATION_START write "\#{" delimiter_state = @token.delimiter_state @@ -625,8 +630,6 @@ module Crystal end end - skip_strings - heredoc_end = @line check :DELIMITER_END @@ -651,15 +654,6 @@ module Crystal false end - private def skip_strings - # Heredocs might indice some spaces that are removed - # because of indentation - while @token.type == :STRING - write @token.raw - next_string_token - end - end - private def consume_heredocs @consuming_heredocs = true @lexer.heredocs.reverse! @@ -997,7 +991,9 @@ module Crystal StringLiteral.new(name).accept self else write @token + @lexer.wants_symbol = false next_token + @lexer.wants_symbol = true end end @@ -1112,6 +1108,7 @@ module Crystal if node.question? node.type_vars[0].accept self + skip_space write_token :"?" return false end @@ -1232,22 +1229,22 @@ module Crystal # Restore the old parentheses count @paren_count = old_paren_count - check_close_paren - false + ensure + check_close_paren end def visit(node : Union) + check_open_paren + if @token.type == :IDENT && @token.value == "self?" && node.types.size == 2 && - node.types.any?(&.is_a?(Self)) && - node.types.any? { |t| t.to_s == "::Nil" } + node.types[0].is_a?(Self) && node.types[1].to_s == "::Nil" write "self?" next_token + check_close_paren return false end - check_open_paren - paren_count = @paren_count column = @column @@ -1391,7 +1388,7 @@ module Crystal def format_nested(node, indent = @indent, write_end_line = true, write_indent = true) slash_is_regex! if node.is_a?(Nop) - skip_nop(indent + 2) + skip_space_write_line else if write_indent indent(indent + 2) do @@ -2339,7 +2336,9 @@ module Crystal end def visit(node : Self) + check_open_paren write_keyword :self + check_close_paren false end @@ -2913,18 +2912,37 @@ module Crystal old_inside_call_or_assign = @inside_call_or_assign @inside_call_or_assign = 0 + comma_before_comment = false + if @token.type == :"," - needs_comma = true - next_token_skip_space_or_newline + next_token + next_token if @token.type == :SPACE + if @token.type == :COMMENT + write "," + needs_comma = false + comma_before_comment = true + @indent += 2 + else + needs_comma = true + end + skip_space_or_newline + @indent -= 2 if comma_before_comment end if @token.keyword?(:do) - write " do" + if comma_before_comment + @indent += 2 + write_indent + else + write " " + end + write "do" next_token_skip_space body = format_block_args node.args, node old_implicit_exception_handler_indent, @implicit_exception_handler_indent = @implicit_exception_handler_indent, @indent format_nested_with_end body @implicit_exception_handler_indent = old_implicit_exception_handler_indent + @indent -= 2 elsif @token.type == :"{" write "," if needs_comma write " {" @@ -3003,6 +3021,14 @@ module Crystal clear_object(body) accept body end + when Not + if body.exp.is_a?(Var) + call = Call.new(nil, "!") + accept call + else + clear_object(body) + accept body + end else raise "BUG: unexpected node for &. argument, at #{node.location}, not #{body.class}" end @@ -3043,6 +3069,12 @@ module Crystal else clear_object(node.obj) end + when Not + if node.exp.is_a?(Var) + node.exp = Nop.new + else + clear_object(node.exp) + end else # nothing to do end @@ -3613,7 +3645,7 @@ module Crystal slash_is_regex! write_indent - write_keyword :when, " " + write_keyword(node.exhaustive? ? :in : :when, " ") base_indent = @column when_start_line = @line when_start_column = @column @@ -4421,7 +4453,7 @@ module Crystal indent(indent) { skip_space(write_comma) } end - def skip_space_or_newline(last : Bool = false, at_least_one : Bool = false) + def skip_space_or_newline(last : Bool = false, at_least_one : Bool = false, next_comes_end : Bool = false) just_wrote_line = @wrote_newline base_column = @column has_space = false @@ -4454,15 +4486,15 @@ module Crystal write_line end end - write_comment(needs_indent: !needs_space) + write_comment(needs_indent: !needs_space, next_comes_end: last) true else false end end - def skip_space_or_newline(indent : Int32, last : Bool = false, at_least_one : Bool = false) - indent(indent) { skip_space_or_newline(last, at_least_one) } + def skip_space_or_newline(indent : Int32, last : Bool = false, at_least_one : Bool = false, next_comes_end : Bool = false) + indent(indent) { skip_space_or_newline(last, at_least_one, next_comes_end) } end def slash_is_regex! @@ -4479,11 +4511,6 @@ module Crystal found_comment end - def skip_nop(indent) - skip_space_write_line - skip_space_or_newline(indent) - end - def skip_semicolon while @token.type == :";" next_token @@ -4524,7 +4551,7 @@ module Crystal skip_space_or_newline end - def write_comment(needs_indent = true, consume_newline = true) + def write_comment(needs_indent = true, consume_newline = true, next_comes_end = false) while @token.type == :COMMENT empty_line = @line_output.to_s.strip.empty? if empty_line @@ -4577,18 +4604,18 @@ module Crystal write value next_token_skip_space if consume_newline - consume_newlines + consume_newlines(next_comes_end: next_comes_end) skip_space_or_newline end end end - def consume_newlines + def consume_newlines(next_comes_end = false) if @token.type == :NEWLINE write_line unless @wrote_newline next_token_skip_space - if @token.type == :NEWLINE + if @token.type == :NEWLINE && !next_comes_end write_line @wrote_double_newlines = true end @@ -4641,7 +4668,7 @@ module Crystal end def write_indent(indent, node) - write_indent(indent) + write_indent(indent) unless node.is_a?(Nop) indent(indent, node) end @@ -4661,6 +4688,7 @@ module Crystal end @wrote_newline = false + @wrote_double_newlines = false @last_write = string end diff --git a/src/compiler/crystal/tools/init.cr b/src/compiler/crystal/tools/init.cr index 2b6486612764..1c97b0bc79a7 100644 --- a/src/compiler/crystal/tools/init.cr +++ b/src/compiler/crystal/tools/init.cr @@ -98,7 +98,7 @@ module Crystal private def self.git_config(key) String.build do |io| - Process.run("git", ["--config", key], output: io) + Process.run("git", ["config", "--get", key], output: io) end.strip.presence end diff --git a/src/compiler/crystal/tools/init/template/license.ecr b/src/compiler/crystal/tools/init/template/license.ecr index 64585cbc4244..3f72be5b86ac 100644 --- a/src/compiler/crystal/tools/init/template/license.ecr +++ b/src/compiler/crystal/tools/init/template/license.ecr @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) <%= Time.local.year %> <%= config.author %> +Copyright (c) <%= Time.local.year %> <%= config.author %> <<%= config.email %>> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/compiler/crystal/tools/playground/agent_instrumentor_transformer.cr b/src/compiler/crystal/tools/playground/agent_instrumentor_transformer.cr index e009bf1a8804..54ffec6cb6d4 100644 --- a/src/compiler/crystal/tools/playground/agent_instrumentor_transformer.cr +++ b/src/compiler/crystal/tools/playground/agent_instrumentor_transformer.cr @@ -61,7 +61,7 @@ module Crystal # so the instrumentor can ignore call's of methods with this name # this will avoid instrumenting calls to methods with the same name than # declared macros in the playground source. For a more accurate solution - # a compilation should be done to distigush whether each call refers to a macro or + # a compilation should be done to distinguish whether each call refers to a macro or # a method. Between the macro names collection and only instrumenting def's inside # modules/classes the generated instrumentation is pretty good enough. See #2355 collector = MacroDefNameCollector.new diff --git a/src/compiler/crystal/types.cr b/src/compiler/crystal/types.cr index 18f56bafaf45..affb0c69a546 100644 --- a/src/compiler/crystal/types.cr +++ b/src/compiler/crystal/types.cr @@ -3,6 +3,8 @@ require "./syntax/ast" module Crystal # Abstract base class of all types abstract class Type + include Annotatable + # Returns the program where this type belongs. getter program @@ -710,23 +712,6 @@ module Crystal end end - # Adds an annotation with the given type and value - def add_annotation(annotation_type : AnnotationType, value : Annotation) - annotations = @annotations ||= {} of AnnotationType => Array(Annotation) - annotations[annotation_type] ||= [] of Annotation - annotations[annotation_type] << value - end - - # Returns the last defined annotation with the given type, if any, or `nil` otherwise - def annotation(annotation_type) : Annotation? - @annotations.try &.[annotation_type]?.try &.last? - end - - # Returns all annotations with the given type, if any, or `nil` otherwise - def annotations(annotation_type) : Array(Annotation)? - @annotations.try &.[annotation_type]? - end - def get_instance_var_initializer(name) nil end @@ -910,6 +895,8 @@ module Crystal end def add_macro(a_macro) + a_macro.owner = self + case a_macro.name when "inherited" return add_hook :inherited, a_macro @@ -1631,7 +1618,7 @@ module Crystal # Check if automatic cast can be done if instance_var.type != value.type && (value.is_a?(NumberLiteral) || value.is_a?(SymbolLiteral)) - if casted_value = MainVisitor.check_automatic_cast(value, instance_var.type) + if casted_value = MainVisitor.check_automatic_cast(@program, value, instance_var.type) value = casted_value end end @@ -1793,7 +1780,7 @@ module Crystal super if generic_args io << '(' - type_vars.join(", ", io, &.to_s(io)) + type_vars.join(io, ", ", &.to_s(io)) io << ')' end end @@ -1853,7 +1840,7 @@ module Crystal super if generic_args io << '(' - type_vars.join(", ", io, &.to_s(io)) + type_vars.join(io, ", ", &.to_s(io)) io << ')' end end @@ -1980,26 +1967,28 @@ module Crystal def to_s_with_options(io : IO, skip_union_parens : Bool = false, generic_args : Bool = true, codegen : Bool = false) : Nil generic_type.append_full_name(io) - io << '(' - type_vars.each_value.with_index do |type_var, i| - io << ", " if i > 0 - if type_var.is_a?(Var) - if i == splat_index - tuple = type_var.type.as(TupleInstanceType) - tuple.tuple_types.join(", ", io) do |tuple_type| - tuple_type = tuple_type.devirtualize unless codegen - tuple_type.to_s_with_options(io, codegen: codegen) + if generic_args + io << '(' + type_vars.each_value.with_index do |type_var, i| + io << ", " if i > 0 + if type_var.is_a?(Var) + if i == splat_index + tuple = type_var.type.as(TupleInstanceType) + tuple.tuple_types.join(io, ", ") do |tuple_type| + tuple_type = tuple_type.devirtualize unless codegen + tuple_type.to_s_with_options(io, codegen: codegen) + end + else + type_var_type = type_var.type + type_var_type = type_var_type.devirtualize unless codegen + type_var_type.to_s_with_options(io, skip_union_parens: true, codegen: codegen) end else - type_var_type = type_var.type - type_var_type = type_var_type.devirtualize unless codegen - type_var_type.to_s_with_options(io, skip_union_parens: true, codegen: codegen) + type_var.to_s(io) end - else - type_var.to_s(io) end + io << ')' end - io << ')' end end @@ -2394,7 +2383,7 @@ module Crystal def to_s_with_options(io : IO, skip_union_parens : Bool = false, generic_args : Bool = true, codegen : Bool = false) : Nil io << "Tuple(" - @tuple_types.join(", ", io) do |tuple_type| + @tuple_types.join(io, ", ") do |tuple_type| tuple_type = tuple_type.devirtualize unless codegen tuple_type.to_s_with_options(io, skip_union_parens: true, codegen: codegen) end @@ -2511,7 +2500,7 @@ module Crystal def to_s_with_options(io : IO, skip_union_parens : Bool = false, generic_args : Bool = true, codegen : Bool = false) : Nil io << "NamedTuple(" - @entries.join(", ", io) do |entry| + @entries.join(io, ", ") do |entry| if Symbol.needs_quotes?(entry.name) entry.name.inspect(io) else @@ -3061,7 +3050,7 @@ module Crystal union_types = @union_types.dup union_types << union_types.delete_at(nil_type_index) end - union_types.join(" | ", io) do |type| + union_types.join(io, " | ") do |type| type = type.devirtualize unless codegen type.to_s_with_options(io, codegen: codegen) end diff --git a/src/compiler/crystal/util.cr b/src/compiler/crystal/util.cr index fe1ef625cb9c..578396c0c888 100644 --- a/src/compiler/crystal/util.cr +++ b/src/compiler/crystal/util.cr @@ -62,10 +62,8 @@ module Crystal end def self.normalize_path(path) - path_start = ".#{File::SEPARATOR}" - unless path.starts_with?(path_start) || path.starts_with?(File::SEPARATOR) - path = path_start + path - end - path.rstrip(File::SEPARATOR) + path = ::Path[path].normalize + path = ::Path["."] / path unless path.anchor + path.to_s end end diff --git a/src/complex.cr b/src/complex.cr index 44772543fb7b..292cbf6f733c 100644 --- a/src/complex.cr +++ b/src/complex.cr @@ -225,9 +225,9 @@ struct Complex log / Math::LOG10 end - # Returns absolute value of `self`. + # Returns `self`. def + - Complex.new(@real.abs, @imag.abs) + self end # Adds the value of `self` to *other*. diff --git a/src/compress/deflate/writer.cr b/src/compress/deflate/writer.cr index 68d7efff5e88..c09b6da2da76 100644 --- a/src/compress/deflate/writer.cr +++ b/src/compress/deflate/writer.cr @@ -58,6 +58,7 @@ class Compress::Deflate::Writer < IO return if @closed consume_output LibZ::Flush::SYNC_FLUSH + @output.flush end # Closes this writer. Must be invoked after all data has been written. diff --git a/src/crc32/crc32.cr b/src/crc32/crc32.cr deleted file mode 100644 index 7af25e8a54ea..000000000000 --- a/src/crc32/crc32.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "digest" - -module CRC32 - @[Deprecated("Use `Digest::CRC32.initial` instead")] - def self.initial : UInt32 - Digest::CRC32.initial - end - - @[Deprecated("Use `Digest::CRC32.checksum` instead")] - def self.checksum(data) : UInt32 - Digest::CRC32.checksum(data) - end - - @[Deprecated("Use `Digest::CRC32.update` instead")] - def self.update(data, crc32 : UInt32) : UInt32 - Digest::CRC32.update(data, crc32) - end - - @[Deprecated("Use `Digest::CRC32.combine` instead")] - def self.combine(crc1 : UInt32, crc2 : UInt32, len) : UInt32 - Digest::CRC32.combine(crc1, crc2, len) - end -end diff --git a/src/crystal/at_exit_handlers.cr b/src/crystal/at_exit_handlers.cr index 8ef75c1430a1..7cd7b8cbc244 100644 --- a/src/crystal/at_exit_handlers.cr +++ b/src/crystal/at_exit_handlers.cr @@ -1,18 +1,12 @@ # :nodoc: module Crystal::AtExitHandlers - @@running = false - private class_getter(handlers) { [] of Int32, ::Exception? -> } def self.add(handler) - raise "Cannot use at_exit from an at_exit handler" if @@running - handlers << handler end def self.run(status, exception = nil) - @@running = true - if handlers = @@handlers # Run the registered handlers in reverse order while handler = handlers.pop? diff --git a/src/crystal/compiler_rt/mulodi4.cr b/src/crystal/compiler_rt/mulodi4.cr index b592a91b69f7..e853b89b13aa 100644 --- a/src/crystal/compiler_rt/mulodi4.cr +++ b/src/crystal/compiler_rt/mulodi4.cr @@ -1,3 +1,4 @@ +# :nodoc: fun __mulodi4(a : Int64, b : Int64, overflow : Int32*) : Int64 n = 64 min = Int64::MIN diff --git a/src/crystal/datum.cr b/src/crystal/datum.cr index e9844fcd08ff..d034a9cf3748 100644 --- a/src/crystal/datum.cr +++ b/src/crystal/datum.cr @@ -5,7 +5,7 @@ module Crystal # Raises otherwise. def as_{{short.id}} : {{type}} {% if immutable == true %} - @raw.as({{type}}).dup + @raw.as({{type}}).clone {% else %} @raw.as({{type}}) {% end %} @@ -16,7 +16,7 @@ module Crystal # Returns `nil` otherwise. def as_{{short.id}}? : {{type}}? {% if immutable == true %} - @raw.as?({{type}}).dup + @raw.as?({{type}}).clone {% else %} @raw.as?({{type}}) {% end %} @@ -29,7 +29,7 @@ module Crystal # # * **types**: contains a named tuple of prefixes and datatypes of each leaf # * **hash_key_type** specifies the type used as the key of `Hash` - # * **immutable**: will generate honor immutability of the values via `.dup` + # * **immutable**: will generate honor immutability of the values via `.clone` # * **target_type**: is the type where the macro is invoked (it's a workaround for #9099). # :nodoc: @@ -106,30 +106,27 @@ module Crystal # Traverses the depth of a structure and returns the value. # Returns `nil` if not found. - def dig?(index_or_key, *subkeys) - if value = self[index_or_key]? - value.dig?(*subkeys) - end + def dig?(index_or_key, *subkeys) : self? + self[index_or_key]?.try &.dig?(*subkeys) end # :nodoc: - def dig?(index_or_key) + def dig?(index_or_key) : self? case @raw when Hash, Array self[index_or_key]? + else + nil end end # Traverses the depth of a structure and returns the value, otherwise raises. - def dig(index_or_key, *subkeys) - if (value = self[index_or_key]) && value.responds_to?(:dig) - return value.dig(*subkeys) - end - raise "#{self.class} value not diggable for key: #{index_or_key.inspect}" + def dig(index_or_key, *subkeys) : self + self[index_or_key].dig(*subkeys) end # :nodoc: - def dig(index_or_key) + def dig(index_or_key) : self self[index_or_key] end diff --git a/src/crystal/dwarf/line_numbers.cr b/src/crystal/dwarf/line_numbers.cr index b44ab51e897f..6d303f3f88cd 100644 --- a/src/crystal/dwarf/line_numbers.cr +++ b/src/crystal/dwarf/line_numbers.cr @@ -159,7 +159,7 @@ module Crystal end end - # Matrix of decompressed `Row` to search line number informations from the + # Matrix of decompressed `Row` to search line number information from the # address of an instruction. # # The matrix contains indexed references to `directories` and `files` to @@ -168,7 +168,7 @@ module Crystal @offset : LibC::OffT - def initialize(@io : IO::FileDescriptor, size) + def initialize(@io : IO::FileDescriptor, size, @base_address : LibC::SizeT = 0) @offset = @io.tell @matrix = Array(Array(Row)).new decode_sequences(size) @@ -366,7 +366,7 @@ module Crystal path = sequence.include_directories[file[1]] row = Row.new( - registers.address, + registers.address + @base_address, registers.op_index, path, file[0], diff --git a/src/crystal/iconv.cr b/src/crystal/iconv.cr index eaac0a8e74a3..9985e361664d 100644 --- a/src/crystal/iconv.cr +++ b/src/crystal/iconv.cr @@ -10,7 +10,7 @@ struct Crystal::Iconv original_from, original_to = from, to @skip_invalid = invalid == :skip - {% unless flag?(:freebsd) || flag?(:musl) || flag?(:dragonfly) %} + {% unless flag?(:freebsd) || flag?(:musl) || flag?(:dragonfly) || flag?(:netbsd) %} if @skip_invalid from = "#{from}//IGNORE" to = "#{to}//IGNORE" diff --git a/src/crystal/mach_o.cr b/src/crystal/mach_o.cr index 02d906cc7ef3..8209452fd3b4 100644 --- a/src/crystal/mach_o.cr +++ b/src/crystal/mach_o.cr @@ -441,11 +441,15 @@ module Crystal end def uuid - @uuid ||= seek_to(LoadCommand::UUID) do - bytes = uninitialized UInt8[16] - @io.read_fully(bytes.to_slice) - UUID.new(bytes) - end.not_nil! + @uuid ||= begin + # ld has a -no_version_load_command options to suppress the LC_UUID entry. + # The Dwarf file generated in such scenario has a LC_UUID = 00000000-0000-0000-0000-000000000000. + seek_to(LoadCommand::UUID) do + bytes = uninitialized UInt8[16] + @io.read_fully(bytes.to_slice) + UUID.new(bytes) + end || UUID.new(StaticArray(UInt8, 16).new(0)) + end end def symbols diff --git a/src/crystal/main.cr b/src/crystal/main.cr index a5c7ed00b369..641d1dd1402d 100644 --- a/src/crystal/main.cr +++ b/src/crystal/main.cr @@ -79,7 +79,7 @@ module Crystal # # ``` # fun main(argc : Int32, argv : UInt8**) : Int32 - # LibFoo.init_foo_and_invoke_main(argc, argv, ->Crystal.main) + # LibFoo.init_foo_and_invoke_main(argc, argv, ->Crystal.main(Int32, UInt8**)) # end # ``` # diff --git a/src/crystal/once.cr b/src/crystal/once.cr index fc915198b6c1..1e6243669809 100644 --- a/src/crystal/once.cr +++ b/src/crystal/once.cr @@ -40,10 +40,12 @@ class Crystal::OnceState {% end %} end +# :nodoc: fun __crystal_once_init : Void* Crystal::OnceState.new.as(Void*) end +# :nodoc: fun __crystal_once(state : Void*, flag : Bool*, initializer : Void*) state.as(Crystal::OnceState).once(flag, initializer) end diff --git a/src/crystal/system.cr b/src/crystal/system.cr index 805ac04b6654..88827a3f6552 100644 --- a/src/crystal/system.cr +++ b/src/crystal/system.cr @@ -11,7 +11,7 @@ end {% if flag?(:unix) %} require "./system/unix/hostname" - {% if flag?(:freebsd) || flag?(:openbsd) || flag?(:dragonfly) %} + {% if flag?(:bsd) %} require "./system/unix/sysctl_cpucount" {% else %} require "./system/unix/sysconf_cpucount" diff --git a/src/crystal/system/random.cr b/src/crystal/system/random.cr index e03cc6ca6118..4581a3011f33 100644 --- a/src/crystal/system/random.cr +++ b/src/crystal/system/random.cr @@ -12,7 +12,7 @@ end {% if flag?(:linux) %} require "./unix/getrandom" -{% elsif flag?(:openbsd) %} +{% elsif flag?(:openbsd) || flag?(:netbsd) %} require "./unix/arc4random" {% elsif flag?(:unix) %} require "./unix/urandom" diff --git a/src/crystal/system/unix/arc4random.cr b/src/crystal/system/unix/arc4random.cr index 67aa0867560a..4338e3341974 100644 --- a/src/crystal/system/unix/arc4random.cr +++ b/src/crystal/system/unix/arc4random.cr @@ -1,11 +1,9 @@ -{% skip_file unless flag?(:openbsd) %} +{% skip_file unless flag?(:openbsd) || flag?(:netbsd) %} require "c/stdlib" module Crystal::System::Random # Fills *buffer* with random bytes using arc4random. - # - # NOTE: only secure on OpenBSD and CloudABI def self.random_bytes(buffer : Bytes) : Nil LibC.arc4random_buf(buffer.to_unsafe.as(Void*), buffer.size) end diff --git a/src/crystal/system/unix/file_descriptor.cr b/src/crystal/system/unix/file_descriptor.cr index 08f21e25ad26..10a1bbe0e772 100644 --- a/src/crystal/system/unix/file_descriptor.cr +++ b/src/crystal/system/unix/file_descriptor.cr @@ -90,7 +90,7 @@ module Crystal::System::FileDescriptor end private def system_reopen(other : IO::FileDescriptor) - {% if LibC.methods.includes? "dup3".id %} + {% if LibC.has_method?("dup3") %} # dup doesn't copy the CLOEXEC flag, so copy it manually using dup3 flags = other.close_on_exec? ? LibC::O_CLOEXEC : 0 if LibC.dup3(other.fd, fd, flags) == -1 @@ -161,4 +161,24 @@ module Crystal::System::FileDescriptor bytes_read end + + def self.from_stdio(fd) + # If we have a TTY for stdin/out/err, it is possibly a shared terminal. + # We need to reopen it to use O_NONBLOCK without causing other programs to break + + # Figure out the terminal TTY name. If ttyname fails we have a non-tty, or something strange. + # For non-tty we set flush_on_newline to true for reasons described in STDOUT and STDERR docs. + path = uninitialized UInt8[256] + ret = LibC.ttyname_r(fd, path, 256) + return IO::FileDescriptor.new(fd).tap(&.flush_on_newline=(true)) unless ret == 0 + + clone_fd = LibC.open(path, LibC::O_RDWR) + return IO::FileDescriptor.new(fd).tap(&.flush_on_newline=(true)) if clone_fd == -1 + + # We don't buffer output for TTY devices to see their output right away + io = IO::FileDescriptor.new(clone_fd) + io.close_on_exec = true + io.sync = true + io + end end diff --git a/src/crystal/system/unix/lib_event2.cr b/src/crystal/system/unix/lib_event2.cr index f898c528bf27..b219aec307cc 100644 --- a/src/crystal/system/unix/lib_event2.cr +++ b/src/crystal/system/unix/lib_event2.cr @@ -7,11 +7,17 @@ require "c/netdb" {% if flag?(:openbsd) %} @[Link("event_core")] @[Link("event_extra")] +{% elsif compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %} + @[Link("event", pkg_config: "libevent")] {% else %} @[Link("event")] {% end %} {% if flag?(:preview_mt) %} - @[Link("event_pthreads")] + {% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %} + @[Link("event_pthreads", pkg_config: "libevent_pthreads")] + {% else %} + @[Link("event_pthreads")] + {% end %} {% end %} lib LibEvent2 alias Int = LibC::Int diff --git a/src/crystal/system/unix/process.cr b/src/crystal/system/unix/process.cr index 3c4828e455d8..dc62f2db7bd9 100644 --- a/src/crystal/system/unix/process.cr +++ b/src/crystal/system/unix/process.cr @@ -2,6 +2,7 @@ require "c/signal" require "c/stdlib" require "c/sys/resource" require "c/unistd" +require "file/error" struct Crystal::System::Process getter pid : LibC::PidT @@ -123,8 +124,11 @@ struct Crystal::System::Process begin reader_pipe.close writer_pipe.close_on_exec = true - self.replace(command_args, env, clear_env, input, output, error, chdir) + self.try_replace(command_args, env, clear_env, input, output, error, chdir) + writer_pipe.write_byte(1) + writer_pipe.write_bytes(Errno.value.to_i) rescue ex + writer_pipe.write_byte(0) writer_pipe.write_bytes(ex.message.try(&.bytesize) || 0) writer_pipe << ex.message writer_pipe.close @@ -134,16 +138,28 @@ struct Crystal::System::Process end writer_pipe.close - bytes = uninitialized UInt8[4] - if reader_pipe.read(bytes.to_slice) == 4 - message_size = IO::ByteFormat::SystemEndian.decode(Int32, bytes.to_slice) - if message_size > 0 - message = String.build(message_size) { |io| IO.copy(reader_pipe, io, message_size) } + begin + case reader_pipe.read_byte + when nil + # Pipe was closed, no error + when 0 + # Error message coming + message_size = reader_pipe.read_bytes(Int32) + if message_size > 0 + message = String.build(message_size) { |io| IO.copy(reader_pipe, io, message_size) } + end + reader_pipe.close + raise RuntimeError.new("Error executing process: '#{command_args[0]}': #{message}") + when 1 + # Errno coming + errno = Errno.new(reader_pipe.read_bytes(Int32)) + self.raise_exception_from_errno(command_args[0], errno) + else + raise RuntimeError.new("BUG: Invalid error response received from subprocess") end + ensure reader_pipe.close - raise RuntimeError.new("Error executing process: #{message}") end - reader_pipe.close pid end @@ -173,7 +189,7 @@ struct Crystal::System::Process end end - def self.replace(command_args, env, clear_env, input, output, error, chdir) : NoReturn + private def self.try_replace(command_args, env, clear_env, input, output, error, chdir) reopen_io(input, ORIGINAL_STDIN) reopen_io(output, ORIGINAL_STDOUT) reopen_io(error, ORIGINAL_STDERR) @@ -194,7 +210,20 @@ struct Crystal::System::Process argv << Pointer(UInt8).null LibC.execvp(command, argv) - raise RuntimeError.from_errno + end + + def self.replace(command_args, env, clear_env, input, output, error, chdir) + try_replace(command_args, env, clear_env, input, output, error, chdir) + raise_exception_from_errno(command_args[0]) + end + + private def self.raise_exception_from_errno(command, errno = Errno.value) + case errno + when Errno::EACCES, Errno::ENOENT + raise ::File::Error.from_errno("Error executing process", errno, file: command) + else + raise IO::Error.from_errno("Error executing process: '#{command}'", errno) + end end private def self.reopen_io(src_io : IO::FileDescriptor, dst_io : IO::FileDescriptor) diff --git a/src/crystal/system/unix/pthread.cr b/src/crystal/system/unix/pthread.cr index 67e7979cbd70..df9fc026447d 100644 --- a/src/crystal/system/unix/pthread.cr +++ b/src/crystal/system/unix/pthread.cr @@ -140,7 +140,7 @@ class Thread {% if flag?(:darwin) %} # FIXME: pthread_get_stacksize_np returns bogus value on macOS X 10.9.0: address = LibC.pthread_get_stackaddr_np(@th) - LibC.pthread_get_stacksize_np(@th) - {% elsif flag?(:freebsd) || flag?(:dragonfly) %} + {% elsif flag?(:bsd) && !flag?(:openbsd) %} ret = LibC.pthread_attr_init(out attr) unless ret == 0 LibC.pthread_attr_destroy(pointerof(attr)) @@ -178,3 +178,19 @@ class Thread @th end end + +# In musl (alpine) the calls to unwind API segfaults +# when the binary is statically linked. This is because +# some symbols like `pthread_once` are defined as "weak" +# and, for some reason, not linked into the final binary. +# Adding an explicit reference to the symbol ensures it's +# included in the statically linked binary. +{% if flag?(:musl) && flag?(:static) %} + lib LibC + fun pthread_once(Void*, Void*) + end + + fun __crystal_static_musl_workaround + LibC.pthread_once(nil, nil) + end +{% end %} diff --git a/src/crystal/system/unix/sysconf_cpucount.cr b/src/crystal/system/unix/sysconf_cpucount.cr index 728f5bf80d18..83dea23c0593 100644 --- a/src/crystal/system/unix/sysconf_cpucount.cr +++ b/src/crystal/system/unix/sysconf_cpucount.cr @@ -1,4 +1,4 @@ -{% skip_file if flag?(:openbsd) || flag?(:freebsd) || flag?(:dragonfly) %} +{% skip_file if flag?(:bsd) %} require "c/unistd" diff --git a/src/crystal/system/unix/sysctl_cpucount.cr b/src/crystal/system/unix/sysctl_cpucount.cr index 2c818c4c0343..496ace615a50 100644 --- a/src/crystal/system/unix/sysctl_cpucount.cr +++ b/src/crystal/system/unix/sysctl_cpucount.cr @@ -1,4 +1,4 @@ -{% skip_file unless flag?(:openbsd) || flag?(:freebsd) || flag?(:dragonfly) %} +{% skip_file unless flag?(:bsd) %} require "c/sysctl" diff --git a/src/crystal/system/unix/time.cr b/src/crystal/system/unix/time.cr index 5d15f5275070..9af57648aa26 100644 --- a/src/crystal/system/unix/time.cr +++ b/src/crystal/system/unix/time.cr @@ -14,7 +14,7 @@ module Crystal::System::Time UnixEpochInSeconds = 62135596800_i64 def self.compute_utc_seconds_and_nanoseconds : {Int64, Int32} - {% if LibC.methods.includes?("clock_gettime".id) %} + {% if LibC.has_method?("clock_gettime") %} ret = LibC.clock_gettime(LibC::CLOCK_REALTIME, out timespec) raise RuntimeError.from_errno("clock_gettime") unless ret == 0 {timespec.tv_sec.to_i64 + UnixEpochInSeconds, timespec.tv_nsec.to_i} diff --git a/src/crystal/system/unix/urandom.cr b/src/crystal/system/unix/urandom.cr index f2998b772fb0..7ac025f43e6b 100644 --- a/src/crystal/system/unix/urandom.cr +++ b/src/crystal/system/unix/urandom.cr @@ -1,4 +1,4 @@ -{% skip_file unless flag?(:unix) && !flag?(:openbsd) && !flag?(:linux) %} +{% skip_file unless flag?(:unix) && !flag?(:netbsd) && !flag?(:openbsd) && !flag?(:linux) %} module Crystal::System::Random @@initialized = false diff --git a/src/crystal/system/win32/env.cr b/src/crystal/system/win32/env.cr index b908dfef1b4e..baad80a01234 100644 --- a/src/crystal/system/win32/env.cr +++ b/src/crystal/system/win32/env.cr @@ -65,13 +65,27 @@ module Crystal::System::Env begin while !pointer.value.zero? string, pointer = String.from_utf16(pointer) - key_value = string.split('=', 2) - key = key_value[0] - value = key_value[1]? || "" + key, _, value = string.partition('=') + # The actual env variables are preceded by these weird lines in the output: + # "=::=::\", "=C:=c:\foo\bar", "=ExitCode=00000000" -- skip them. + next if key.empty? yield key, value end ensure LibC.FreeEnvironmentStringsW(orig_pointer) end end + + # Used internally to create an input for `CreateProcess` `lpEnvironment`. + def self.make_env_block(env : Enumerable({String, String})) + String.build do |io| + env.each do |(key, value)| + if key.includes?('=') || key.empty? + raise ArgumentError.new("Invalid env key #{key.inspect}") + end + io << key.check_no_null_byte("key") << '=' << value.check_no_null_byte("value") << '\0' + end + io << '\0' + end.to_utf16.to_unsafe + end end diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr index b6b9c2d51396..716c285030e1 100644 --- a/src/crystal/system/win32/file.cr +++ b/src/crystal/system/win32/file.cr @@ -193,7 +193,7 @@ module Crystal::System::File def self.symlink(old_path : String, new_path : String) : Nil # TODO: support directory symlinks (copy Go's stdlib logic here) - if LibC.CreateSymbolicLinkW(to_windows_path(new_path), to_windows_path(old_path), 0) == 0 + if LibC.CreateSymbolicLinkW(to_windows_path(new_path), to_windows_path(old_path), LibC::SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0 raise ::File::Error.from_winerror("Error creating symbolic link", file: old_path, other: new_path) end end @@ -209,12 +209,26 @@ module Crystal::System::File end def self.utime(access_time : ::Time, modification_time : ::Time, path : String) : Nil - times = LibC::Utimbuf64.new - times.actime = access_time.to_unix - times.modtime = modification_time.to_unix - - if LibC._wutime64(to_windows_path(path), pointerof(times)) != 0 - raise ::File::Error.from_errno("Error setting time on file", file: path) + atime = Crystal::System::Time.to_filetime(access_time) + mtime = Crystal::System::Time.to_filetime(modification_time) + handle = LibC.CreateFileW( + to_windows_path(path), + LibC::FILE_WRITE_ATTRIBUTES, + LibC::FILE_SHARE_READ | LibC::FILE_SHARE_WRITE | LibC::FILE_SHARE_DELETE, + nil, + LibC::OPEN_EXISTING, + LibC::FILE_ATTRIBUTE_NORMAL, + LibC::HANDLE.null + ) + if handle == LibC::INVALID_HANDLE_VALUE + raise ::File::Error.from_winerror("Error setting time on file", file: path) + end + begin + if LibC.SetFileTime(handle, nil, pointerof(atime), pointerof(mtime)) == 0 + raise ::File::Error.from_winerror("Error setting time on file", file: path) + end + ensure + LibC.CloseHandle(handle) end end @@ -241,6 +255,8 @@ module Crystal::System::File end private def system_fsync(flush_metadata = true) : Nil - raise NotImplementedError.new("File#fsync") + if LibC._commit(fd) != 0 + raise IO::Error.from_errno("Error syncing file") + end end end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index 12d85d6c6054..0fe008c93814 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -1,4 +1,5 @@ require "c/io" +require "c/consoleapi" module Crystal::System::FileDescriptor @volatile_fd : Atomic(LibC::Int) @@ -96,7 +97,7 @@ module Crystal::System::FileDescriptor end private def system_reopen(other : IO::FileDescriptor) - {% if LibC.methods.includes? "dup3".id %} + {% if LibC.has_method?("dup3") %} # dup doesn't copy the CLOEXEC flag, so copy it manually using dup3 flags = other.close_on_exec? ? LibC::O_CLOEXEC : 0 if LibC.dup3(other.fd, self.fd, flags) == -1 @@ -162,4 +163,24 @@ module Crystal::System::FileDescriptor bytes_read end + + def self.from_stdio(fd) + console_handle = false + handle = LibC._get_osfhandle(fd) + if handle != -1 + if LibC.GetConsoleMode(LibC::HANDLE.new(handle), out _) != 0 + console_handle = true + end + end + + io = IO::FileDescriptor.new(fd) + # Set sync or flush_on_newline as described in STDOUT and STDERR docs. + # See https://crystal-lang.org/api/toplevel.html#STDERR + if console_handle + io.sync = true + else + io.flush_on_newline = true + end + io + end end diff --git a/src/crystal/system/win32/process.cr b/src/crystal/system/win32/process.cr index 0ee56c760cd6..20a0c727837e 100644 --- a/src/crystal/system/win32/process.cr +++ b/src/crystal/system/win32/process.cr @@ -1,4 +1,5 @@ require "c/processthreadsapi" +require "process/shell" struct Crystal::System::Process getter pid : LibC::DWORD @@ -111,10 +112,6 @@ struct Crystal::System::Process end def self.spawn(command_args, env, clear_env, input, output, error, chdir) - if env || clear_env - raise NotImplementedError.new("Process.new with env or clear_env options") - end - startup_info = LibC::STARTUPINFOW.new startup_info.cb = sizeof(LibC::STARTUPINFOW) startup_info.dwFlags = LibC::STARTF_USESTDHANDLES @@ -126,11 +123,17 @@ struct Crystal::System::Process process_info = LibC::PROCESS_INFORMATION.new if LibC.CreateProcessW( - nil, command_args.check_no_null_byte.to_utf16, nil, nil, true, 0, - nil, chdir.try &.check_no_null_byte.to_utf16, + nil, command_args.check_no_null_byte.to_utf16, nil, nil, true, LibC::CREATE_UNICODE_ENVIRONMENT, + make_env_block(env, clear_env), chdir.try &.check_no_null_byte.to_utf16, pointerof(startup_info), pointerof(process_info) ) == 0 - raise RuntimeError.from_winerror("Error executing process") + error = WinError.value + case error.to_errno + when Errno::EACCES, Errno::ENOENT + raise ::File::Error.from_winerror("Error executing process", error, file: command_args) + else + raise IO::Error.from_winerror("Error executing process: '#{command_args}'", error) + end end close_handle(process_info.hThread) @@ -151,35 +154,7 @@ struct Crystal::System::Process else command_args = [command] command_args.concat(args) if args - String.build { |io| args_to_string(command_args, io) } - end - end - - private def self.args_to_string(args, io : IO) - args.join(' ', io) do |arg| - quotes = arg.empty? || arg.includes?(' ') || arg.includes?('\t') - - io << '"' if quotes - - slashes = 0 - arg.each_char do |c| - case c - when '\\' - slashes += 1 - when '"' - (slashes + 1).times { io << '\\' } - slashes = 0 - else - slashes = 0 - end - - io << c - end - - if quotes - slashes.times { io << '\\' } - io << '"' - end + ::Process.quote_windows(command_args) end end @@ -190,6 +165,29 @@ struct Crystal::System::Process def self.chroot(path) raise NotImplementedError.new("Process.chroot") end + + protected def self.make_env_block(env, clear_env : Bool) : UInt16* + # If neither clearing nor adding anything, use the default behavior of inheriting everything. + return Pointer(UInt16).null if !env && !clear_env + + # Emulate case-insensitive behavior using a Hash like {"KEY" => {"kEy", "value"}, ...} + final_env = {} of String => {String, String} + unless clear_env + Crystal::System::Env.each do |key, val| + final_env[key.upcase] = {key, val} + end + end + env.try &.each do |(key, val)| + if val + # Note: in the case of overriding, the last "case-spelling" of the key wins. + final_env[key.upcase] = {key, val} + else + final_env.delete key.upcase + end + end + # The "values" we're passing are actually key-value pairs. + Crystal::System::Env.make_env_block(final_env.each_value) + end end private def close_handle(handle) diff --git a/src/crystal/system/win32/time.cr b/src/crystal/system/win32/time.cr index 535a846d4074..c3afe2cd06ab 100644 --- a/src/crystal/system/win32/time.cr +++ b/src/crystal/system/win32/time.cr @@ -35,6 +35,15 @@ module Crystal::System::Time ::Time.utc(seconds: seconds, nanoseconds: nanoseconds) end + def self.to_filetime(time : ::Time) : LibC::FILETIME + span = time - ::Time.utc(seconds: WINDOWS_EPOCH_IN_SECONDS, nanoseconds: 0) + ticks = span.to_i.to_u64 * FILETIME_TICKS_PER_SECOND + span.nanoseconds // NANOSECONDS_PER_FILETIME_TICK + filetime = uninitialized LibC::FILETIME + filetime.dwHighDateTime = (ticks >> 32).to_u32 + filetime.dwLowDateTime = ticks.to_u32! + filetime + end + def self.filetime_to_f64secs(filetime) : Float64 ((filetime.dwHighDateTime.to_u64 << 32) | filetime.dwLowDateTime.to_u64).to_f64 / FILETIME_TICKS_PER_SECOND.to_f64 end diff --git a/src/deque.cr b/src/deque.cr index e1bbba548c3d..4175269496dc 100644 --- a/src/deque.cr +++ b/src/deque.cr @@ -112,11 +112,6 @@ class Deque(T) equals?(other) { |x, y| x == y } end - # :nodoc: - def ==(other) - false - end - # Concatenation. Returns a new `Deque` built by concatenating # two deques together to create a third. The type of the new deque # is the union of the types of both the other deques. @@ -336,7 +331,7 @@ class Deque(T) def inspect(io : IO) : Nil executed = exec_recursive(:inspect) do io << "Deque{" - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << '}' end io << "Deque{...}" unless executed diff --git a/src/digest/base.cr b/src/digest/base.cr index b2e8a8c83cb0..dd5e542c8970 100644 --- a/src/digest/base.cr +++ b/src/digest/base.cr @@ -1,92 +1,158 @@ require "base64" +class Digest::FinalizedError < Exception +end + abstract class Digest::Base - # Returns the hash of *data*. *data* must respond to `#to_slice`. - def self.digest(data) - digest do |ctx| - ctx.update(data.to_slice) + macro inherited + # Returns the hash of *data*. *data* must respond to `#to_slice`. + def self.digest(data) + digest do |ctx| + ctx.update(data.to_slice) + end + end + + # Yields an instance of `self` which can receive calls to `#update(data : String | Bytes)` + # and returns the finalized digest afterwards. + # + # ``` + # require "digest/md5" + # + # digest = Digest::MD5.digest do |ctx| + # ctx.update "f" + # ctx.update "oo" + # end + # digest.to_slice.hexstring # => "acbd18db4cc2f85cedef654fccc4a4d8" + # ``` + def self.digest(& : self ->) : Bytes + context = new + yield context + context.final + end + + # Returns the hexadecimal representation of the hash of *data*. + # + # ``` + # require "digest/md5" + # + # Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8" + # ``` + def self.hexdigest(data) : String + hexdigest &.update(data) + end + + # Yields a context object with an `#update(data : String | Bytes)` + # method available. Returns the resulting digest in hexadecimal representation + # afterwards. + # + # ``` + # require "digest/md5" + # + # Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8" + # Digest::MD5.hexdigest do |ctx| + # ctx.update "f" + # ctx.update "oo" + # end + # # => "acbd18db4cc2f85cedef654fccc4a4d8" + # ``` + def self.hexdigest(& : self ->) : String + hashsum = digest do |ctx| + yield ctx + end + + hashsum.to_slice.hexstring + end + + # Returns the base64-encoded hash of *data*. + # + # ``` + # require "digest/sha1" + # + # Digest::SHA1.base64digest("foo") # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM=" + # ``` + def self.base64digest(data) : String + base64digest &.update(data) + end + + # Yields a context object with an `#update(data : String | Bytes)` + # method available. Returns the resulting digest in base64 representation + # afterwards. + # + # ``` + # require "digest/sha1" + # + # Digest::SHA1.base64digest do |ctx| + # ctx.update "f" + # ctx.update "oo" + # end + # # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM=" + # ``` + def self.base64digest(& : self -> _) : String + hashsum = digest do |ctx| + yield ctx + end + + Base64.strict_encode(hashsum) end end - # Yields a context object with an `#update(data : String | Bytes)` - # method available. Returns the resulting digest afterwards. - # - # ``` - # require "digest/md5" - # - # digest = Digest::MD5.digest do |ctx| - # ctx.update "f" - # ctx.update "oo" - # end - # digest.to_slice.hexstring # => "acbd18db4cc2f85cedef654fccc4a4d8" - # ``` - def self.digest - context = new - yield context - context.final - context.result + @finished = false + + def update(data) : self + update data.to_slice end - # Returns the hexadecimal representation of the hash of *data*. - # - # ``` - # require "digest/md5" - # - # Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8" - # ``` - def self.hexdigest(data) : String - hexdigest &.update(data) + def update(data : Bytes) : self + check_finished + update_impl data + self end - # Yields a context object with an `#update(data : String | Bytes)` - # method available. Returns the resulting digest in hexadecimal representation - # afterwards. + # Returns the final digest output. # - # ``` - # require "digest/md5" + # This method can only be called once and raises `FinalizedError` on subsequent calls. # - # Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8" - # Digest::MD5.hexdigest do |ctx| - # ctx.update "f" - # ctx.update "oo" - # end - # # => "acbd18db4cc2f85cedef654fccc4a4d8" - # ``` - def self.hexdigest : String - hashsum = digest do |ctx| - yield ctx - end + # NOTE: `.dup.final` call may be used to get an intermediate hash value. + def final : Bytes + dst = Bytes.new digest_size + final dst + end - hashsum.to_slice.hexstring + def final(dst : Bytes) : Bytes + check_finished + @finished = true + final_impl dst + dst end - # Returns the base64-encoded hash of *data*. - # - # ``` - # require "digest/sha1" - # - # Digest::SHA1.base64digest("foo") # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM=" - # ``` - def self.base64digest(data) : String - base64digest &.update(data) + def reset : self + reset_impl + @finished = false + self end - # Returns the base64-encoded hash of *data*. - # - # ``` - # require "digest/sha1" - # - # Digest::SHA1.base64digest do |ctx| - # ctx.update "f" - # ctx.update "oo" - # end - # # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM=" - # ``` - def self.base64digest : String - hashsum = digest do |ctx| - yield ctx - end + # Dups and finishes the digest. + @[Deprecated("Use `final` instead.")] + def digest : Bytes + dup.final + end - Base64.strict_encode(hashsum) + # Returns a hexadecimal-encoded digest. + @[Deprecated("Use `final.hexstring` instead.")] + def hexdigest : String + digest.hexstring end + + private def check_finished : Nil + raise FinalizedError.new("finish already called") if @finished + end + + # Hashes data incrementally. + abstract def update_impl(data : Bytes) : Nil + # Stores the output digest of #digest_size bytes in dst. + abstract def final_impl(dst : Bytes) : Nil + # Resets the object to it's initial state. + abstract def reset_impl : Nil + # Returns the digest output size in bytes. + abstract def digest_size : Int32 end diff --git a/src/digest/md5.cr b/src/digest/md5.cr index 23ef552ce8d9..90dfdaa93ea0 100644 --- a/src/digest/md5.cr +++ b/src/digest/md5.cr @@ -7,32 +7,37 @@ require "./base" # `Crypto::Bcrypt::Password`. For a generic cryptographic hash, use SHA-256 via # `OpenSSL::Digest.new("SHA256")`. class Digest::MD5 < Digest::Base + @i = StaticArray(UInt32, 2).new(0_u32) + @buf = StaticArray(UInt32, 4).new(0_u32) + @in = StaticArray(UInt8, 64).new(0_u8) + def initialize - @i = StaticArray(UInt32, 2).new(0_u32) - @buf = StaticArray(UInt32, 4).new(0_u32) + reset + end + + private def reset_impl : Nil + @i[0] = 0_u32 + @i[1] = 0_u32 @buf[0] = 0x67452301_u32 @buf[1] = 0xEFCDAB89_u32 @buf[2] = 0x98BADCFE_u32 @buf[3] = 0x10325476_u32 - @in = StaticArray(UInt8, 64).new(0_u8) - @digest = uninitialized UInt8[16] end - def update(data) - slice = data.to_slice - update(slice.to_unsafe, slice.bytesize.to_u32) + private def update_impl(data : Bytes) : Nil + update(data.to_unsafe, data.bytesize.to_u32) end - def update(inBuf, inLen) - in = uninitialized UInt32[16] + private def update(inBuf, inLen) + tmp_in = uninitialized UInt32[16] # compute number of bytes mod 64 mdi = (@i[0] >> 3) & 0x3F # update number of bits - @i[1] += 1 if (@i[0] + (inLen << 3)) < @i[0] - @i[0] += (inLen << 3) - @i[1] += (inLen >> 29) + @i[1] &+= 1 if (@i[0] &+ (inLen << 3)) < @i[0] + @i[0] &+= (inLen << 3) + @i[1] &+= (inLen >> 29) inLen.times do # add new character to buffer, increment mdi @@ -44,13 +49,13 @@ class Digest::MD5 < Digest::Base if mdi == 0x40 ii = 0 16.times do |i| - in[i] = (@in[ii + 3].to_u32 << 24) | - (@in[ii + 2].to_u32 << 16) | - (@in[ii + 1].to_u32 << 8) | - (@in[ii]) + tmp_in[i] = (@in[ii + 3].to_u32 << 24) | + (@in[ii + 2].to_u32 << 16) | + (@in[ii + 1].to_u32 << 8) | + (@in[ii]) ii += 4 end - transform in + transform tmp_in mdi = 0 end end @@ -76,130 +81,130 @@ class Digest::MD5 < Digest::Base S43 = 15 S44 = 21 - PADDING = begin + private PADDING = begin padding = StaticArray(UInt8, 64).new(0_u8) padding[0] = 0x80_u8 padding end - def f(x, y, z) + private def f(x, y, z) (x & y) | ((~x) & z) end - def g(x, y, z) + private def g(x, y, z) (x & z) | (y & (~z)) end - def h(x, y, z) + private def h(x, y, z) x ^ y ^ z end - def i(x, y, z) + private def i(x, y, z) y ^ (x | (~z)) end - def rotate_left(x, n) + private def rotate_left(x, n) (x << n) | (x >> (32 - n)) end - def ff(a, b, c, d, x, s, ac) + private def ff(a, b, c, d, x, s, ac) a &+= f(b, c, d) &+ x &+ ac.to_u32 a = rotate_left a, s a &+= b end - def gg(a, b, c, d, x, s, ac) + private def gg(a, b, c, d, x, s, ac) a &+= g(b, c, d) &+ x &+ ac.to_u32 a = rotate_left a, s a &+= b end - def hh(a, b, c, d, x, s, ac) + private def hh(a, b, c, d, x, s, ac) a &+= h(b, c, d) &+ x &+ ac.to_u32 a = rotate_left a, s a &+= b end - def ii(a, b, c, d, x, s, ac) + private def ii(a, b, c, d, x, s, ac) a &+= i(b, c, d) &+ x &+ ac.to_u32 a = rotate_left a, s a &+= b end - def transform(in) + private def transform(input) a, b, c, d = @buf # Round 1 - a = ff(a, b, c, d, in[0], S11, 3614090360) # 1 - d = ff(d, a, b, c, in[1], S12, 3905402710) # 2 - c = ff(c, d, a, b, in[2], S13, 606105819) # 3 - b = ff(b, c, d, a, in[3], S14, 3250441966) # 4 - a = ff(a, b, c, d, in[4], S11, 4118548399) # 5 - d = ff(d, a, b, c, in[5], S12, 1200080426) # 6 - c = ff(c, d, a, b, in[6], S13, 2821735955) # 7 - b = ff(b, c, d, a, in[7], S14, 4249261313) # 8 - a = ff(a, b, c, d, in[8], S11, 1770035416) # 9 - d = ff(d, a, b, c, in[9], S12, 2336552879) # 10 - c = ff(c, d, a, b, in[10], S13, 4294925233) # 11 - b = ff(b, c, d, a, in[11], S14, 2304563134) # 12 - a = ff(a, b, c, d, in[12], S11, 1804603682) # 13 - d = ff(d, a, b, c, in[13], S12, 4254626195) # 14 - c = ff(c, d, a, b, in[14], S13, 2792965006) # 15 - b = ff(b, c, d, a, in[15], S14, 1236535329) # 16 + a = ff(a, b, c, d, input[0], S11, 3614090360) # 1 + d = ff(d, a, b, c, input[1], S12, 3905402710) # 2 + c = ff(c, d, a, b, input[2], S13, 606105819) # 3 + b = ff(b, c, d, a, input[3], S14, 3250441966) # 4 + a = ff(a, b, c, d, input[4], S11, 4118548399) # 5 + d = ff(d, a, b, c, input[5], S12, 1200080426) # 6 + c = ff(c, d, a, b, input[6], S13, 2821735955) # 7 + b = ff(b, c, d, a, input[7], S14, 4249261313) # 8 + a = ff(a, b, c, d, input[8], S11, 1770035416) # 9 + d = ff(d, a, b, c, input[9], S12, 2336552879) # 10 + c = ff(c, d, a, b, input[10], S13, 4294925233) # 11 + b = ff(b, c, d, a, input[11], S14, 2304563134) # 12 + a = ff(a, b, c, d, input[12], S11, 1804603682) # 13 + d = ff(d, a, b, c, input[13], S12, 4254626195) # 14 + c = ff(c, d, a, b, input[14], S13, 2792965006) # 15 + b = ff(b, c, d, a, input[15], S14, 1236535329) # 16 # Round 2 - a = gg(a, b, c, d, in[1], S21, 4129170786) # 17 - d = gg(d, a, b, c, in[6], S22, 3225465664) # 18 - c = gg(c, d, a, b, in[11], S23, 643717713) # 19 - b = gg(b, c, d, a, in[0], S24, 3921069994) # 20 - a = gg(a, b, c, d, in[5], S21, 3593408605) # 21 - d = gg(d, a, b, c, in[10], S22, 38016083) # 22 - c = gg(c, d, a, b, in[15], S23, 3634488961) # 23 - b = gg(b, c, d, a, in[4], S24, 3889429448) # 24 - a = gg(a, b, c, d, in[9], S21, 568446438) # 25 - d = gg(d, a, b, c, in[14], S22, 3275163606) # 26 - c = gg(c, d, a, b, in[3], S23, 4107603335) # 27 - b = gg(b, c, d, a, in[8], S24, 1163531501) # 28 - a = gg(a, b, c, d, in[13], S21, 2850285829) # 29 - d = gg(d, a, b, c, in[2], S22, 4243563512) # 30 - c = gg(c, d, a, b, in[7], S23, 1735328473) # 31 - b = gg(b, c, d, a, in[12], S24, 2368359562) # 32 + a = gg(a, b, c, d, input[1], S21, 4129170786) # 17 + d = gg(d, a, b, c, input[6], S22, 3225465664) # 18 + c = gg(c, d, a, b, input[11], S23, 643717713) # 19 + b = gg(b, c, d, a, input[0], S24, 3921069994) # 20 + a = gg(a, b, c, d, input[5], S21, 3593408605) # 21 + d = gg(d, a, b, c, input[10], S22, 38016083) # 22 + c = gg(c, d, a, b, input[15], S23, 3634488961) # 23 + b = gg(b, c, d, a, input[4], S24, 3889429448) # 24 + a = gg(a, b, c, d, input[9], S21, 568446438) # 25 + d = gg(d, a, b, c, input[14], S22, 3275163606) # 26 + c = gg(c, d, a, b, input[3], S23, 4107603335) # 27 + b = gg(b, c, d, a, input[8], S24, 1163531501) # 28 + a = gg(a, b, c, d, input[13], S21, 2850285829) # 29 + d = gg(d, a, b, c, input[2], S22, 4243563512) # 30 + c = gg(c, d, a, b, input[7], S23, 1735328473) # 31 + b = gg(b, c, d, a, input[12], S24, 2368359562) # 32 # Round 3 - a = hh(a, b, c, d, in[5], S31, 4294588738) # 33 - d = hh(d, a, b, c, in[8], S32, 2272392833) # 34 - c = hh(c, d, a, b, in[11], S33, 1839030562) # 35 - b = hh(b, c, d, a, in[14], S34, 4259657740) # 36 - a = hh(a, b, c, d, in[1], S31, 2763975236) # 37 - d = hh(d, a, b, c, in[4], S32, 1272893353) # 38 - c = hh(c, d, a, b, in[7], S33, 4139469664) # 39 - b = hh(b, c, d, a, in[10], S34, 3200236656) # 40 - a = hh(a, b, c, d, in[13], S31, 681279174) # 41 - d = hh(d, a, b, c, in[0], S32, 3936430074) # 42 - c = hh(c, d, a, b, in[3], S33, 3572445317) # 43 - b = hh(b, c, d, a, in[6], S34, 76029189) # 44 - a = hh(a, b, c, d, in[9], S31, 3654602809) # 45 - d = hh(d, a, b, c, in[12], S32, 3873151461) # 46 - c = hh(c, d, a, b, in[15], S33, 530742520) # 47 - b = hh(b, c, d, a, in[2], S34, 3299628645) # 48 + a = hh(a, b, c, d, input[5], S31, 4294588738) # 33 + d = hh(d, a, b, c, input[8], S32, 2272392833) # 34 + c = hh(c, d, a, b, input[11], S33, 1839030562) # 35 + b = hh(b, c, d, a, input[14], S34, 4259657740) # 36 + a = hh(a, b, c, d, input[1], S31, 2763975236) # 37 + d = hh(d, a, b, c, input[4], S32, 1272893353) # 38 + c = hh(c, d, a, b, input[7], S33, 4139469664) # 39 + b = hh(b, c, d, a, input[10], S34, 3200236656) # 40 + a = hh(a, b, c, d, input[13], S31, 681279174) # 41 + d = hh(d, a, b, c, input[0], S32, 3936430074) # 42 + c = hh(c, d, a, b, input[3], S33, 3572445317) # 43 + b = hh(b, c, d, a, input[6], S34, 76029189) # 44 + a = hh(a, b, c, d, input[9], S31, 3654602809) # 45 + d = hh(d, a, b, c, input[12], S32, 3873151461) # 46 + c = hh(c, d, a, b, input[15], S33, 530742520) # 47 + b = hh(b, c, d, a, input[2], S34, 3299628645) # 48 # Round 4 - a = ii(a, b, c, d, in[0], S41, 4096336452) # 49 - d = ii(d, a, b, c, in[7], S42, 1126891415) # 50 - c = ii(c, d, a, b, in[14], S43, 2878612391) # 51 - b = ii(b, c, d, a, in[5], S44, 4237533241) # 52 - a = ii(a, b, c, d, in[12], S41, 1700485571) # 53 - d = ii(d, a, b, c, in[3], S42, 2399980690) # 54 - c = ii(c, d, a, b, in[10], S43, 4293915773) # 55 - b = ii(b, c, d, a, in[1], S44, 2240044497) # 56 - a = ii(a, b, c, d, in[8], S41, 1873313359) # 57 - d = ii(d, a, b, c, in[15], S42, 4264355552) # 58 - c = ii(c, d, a, b, in[6], S43, 2734768916) # 59 - b = ii(b, c, d, a, in[13], S44, 1309151649) # 60 - a = ii(a, b, c, d, in[4], S41, 4149444226) # 61 - d = ii(d, a, b, c, in[11], S42, 3174756917) # 62 - c = ii(c, d, a, b, in[2], S43, 718787259) # 63 - b = ii(b, c, d, a, in[9], S44, 3951481745) # 64 + a = ii(a, b, c, d, input[0], S41, 4096336452) # 49 + d = ii(d, a, b, c, input[7], S42, 1126891415) # 50 + c = ii(c, d, a, b, input[14], S43, 2878612391) # 51 + b = ii(b, c, d, a, input[5], S44, 4237533241) # 52 + a = ii(a, b, c, d, input[12], S41, 1700485571) # 53 + d = ii(d, a, b, c, input[3], S42, 2399980690) # 54 + c = ii(c, d, a, b, input[10], S43, 4293915773) # 55 + b = ii(b, c, d, a, input[1], S44, 2240044497) # 56 + a = ii(a, b, c, d, input[8], S41, 1873313359) # 57 + d = ii(d, a, b, c, input[15], S42, 4264355552) # 58 + c = ii(c, d, a, b, input[6], S43, 2734768916) # 59 + b = ii(b, c, d, a, input[13], S44, 1309151649) # 60 + a = ii(a, b, c, d, input[4], S41, 4149444226) # 61 + d = ii(d, a, b, c, input[11], S42, 3174756917) # 62 + c = ii(c, d, a, b, input[2], S43, 718787259) # 63 + b = ii(b, c, d, a, input[9], S44, 3951481745) # 64 @buf[0] &+= a @buf[1] &+= b @@ -207,12 +212,12 @@ class Digest::MD5 < Digest::Base @buf[3] &+= d end - def final - in = uninitialized UInt32[16] + private def final_impl(dst : Bytes) : Nil + tmp_in = uninitialized UInt32[16] # save number of bits - in[14] = @i[0] - in[15] = @i[1] + tmp_in[14] = @i[0] + tmp_in[15] = @i[1] # compute number of bytes mod 64 mdi = ((@i[0] >> 3) & 0x3F).to_i32 @@ -224,26 +229,26 @@ class Digest::MD5 < Digest::Base # append length in bits and transform ii = 0 14.times do |i| - in[i] = (@in[ii + 3].to_u32 << 24) | - (@in[ii + 2].to_u32 << 16) | - (@in[ii + 1].to_u32 << 8) | - (@in[ii]) + tmp_in[i] = (@in[ii + 3].to_u32 << 24) | + (@in[ii + 2].to_u32 << 16) | + (@in[ii + 1].to_u32 << 8) | + (@in[ii]) ii += 4 end - transform in + transform tmp_in # store buffer in digest ii = 0 4.times do |i| - @digest[ii] = (@buf[i] & 0xff).to_u8 - @digest[ii + 1] = ((@buf[i] >> 8) & 0xFF).to_u8 - @digest[ii + 2] = ((@buf[i] >> 16) & 0xFF).to_u8 - @digest[ii + 3] = ((@buf[i] >> 24) & 0xFF).to_u8 + dst[ii] = (@buf[i] & 0xff).to_u8 + dst[ii + 1] = ((@buf[i] >> 8) & 0xFF).to_u8 + dst[ii + 2] = ((@buf[i] >> 16) & 0xFF).to_u8 + dst[ii + 3] = ((@buf[i] >> 24) & 0xFF).to_u8 ii += 4 end end - def result - @digest + def digest_size : Int32 + 16 end end diff --git a/src/digest/sha1.cr b/src/digest/sha1.cr index bc6c7fa06d91..b188530088fb 100644 --- a/src/digest/sha1.cr +++ b/src/digest/sha1.cr @@ -10,13 +10,20 @@ class Digest::SHA1 < Digest::Base # This is a direct translation of https://tools.ietf.org/html/rfc3174#section-7 # but we use loop unrolling for faster execution (about 1.07x slower than OpenSSL::SHA1). + @intermediate_hash = uninitialized UInt32[5] + @length_low = 0_u32 + @length_high = 0_u32 + @message_block_index = 0 + @message_block = StaticArray(UInt8, 64).new(0_u8) # uninitialized UInt8[64] + def initialize - @intermediate_hash = uninitialized UInt32[5] + reset + end + + private def reset_impl : Nil @length_low = 0_u32 @length_high = 0_u32 - @message_block = StaticArray(UInt8, 64).new(0_u8) # uninitialized UInt8[64] @message_block_index = 0 - @intermediate_hash[0] = 0x67452301_u32 @intermediate_hash[1] = 0xEFCDAB89_u32 @intermediate_hash[2] = 0x98BADCFE_u32 @@ -24,15 +31,14 @@ class Digest::SHA1 < Digest::Base @intermediate_hash[4] = 0xC3D2E1F0_u32 end - def update(data) - message_array = data.to_slice - message_array.each do |byte| + private def update_impl(data : Bytes) : Nil + data.each do |byte| @message_block[@message_block_index] = byte & 0xFF_u8 @message_block_index += 1 - @length_low += 8 + @length_low &+= 8 if @length_low == 0 - @length_high += 1 + @length_high &+= 1 if @length_high == 0 raise ArgumentError.new "Crypto.sha1: message too long" end @@ -44,7 +50,17 @@ class Digest::SHA1 < Digest::Base end end - def process_message_block + private def final_impl(dst : Bytes) : Nil + pad_message + + @length_low = 0_u32 + @length_high = 0_u32 + {% for i in 0...20 %} + dst[{{i}}] = (@intermediate_hash[{{i >> 2}}] >> 8 * (3 - ({{i & 0x03}}))).to_u8! + {% end %} + end + + private def process_message_block k = {0x5A827999_u32, 0x6ED9EBA1_u32, 0x8F1BBCDC_u32, 0xCA62C1D6_u32} w = uninitialized UInt32[80] @@ -113,27 +129,11 @@ class Digest::SHA1 < Digest::Base @message_block_index = 0 end - def circular_shift(bits, word) + private def circular_shift(bits, word) (word << bits) | (word >> (32 - bits)) end - def final - end - - def result - message_digest = uninitialized UInt8[20] - pad_message - - @length_low = 0_u32 - @length_high = 0_u32 - {% for i in 0...20 %} - message_digest[{{i}}] = (@intermediate_hash[{{i >> 2}}] >> 8 * (3 - ({{i & 0x03}}))).to_u8! - {% end %} - - message_digest - end - - def pad_message + private def pad_message if @message_block_index > 55 @message_block[@message_block_index] = 0x80_u8 @message_block_index += 1 @@ -168,4 +168,8 @@ class Digest::SHA1 < Digest::Base process_message_block end + + def digest_size : Int32 + 20 + end end diff --git a/src/dir.cr b/src/dir.cr index 60934bf658b5..cac0382f88f9 100644 --- a/src/dir.cr +++ b/src/dir.cr @@ -249,8 +249,14 @@ class Dir end # Removes the directory at the given path. - def self.rmdir(path : Path | String) : Nil - Crystal::System::Dir.delete(path) + @[Deprecated("Use `Dir.delete` instead")] + def self.rmdir(path : Path | String) + delete(path) + end + + # Removes the directory at the given path. + def self.delete(path : Path | String) + Crystal::System::Dir.delete(path.to_s) end def to_s(io : IO) : Nil diff --git a/src/dir/glob.cr b/src/dir/glob.cr index b6d86952bb99..4c006320806d 100644 --- a/src/dir/glob.cr +++ b/src/dir/glob.cr @@ -4,12 +4,12 @@ class Dir # The pattern syntax is similar to shell filename globbing, see `File.match?` for details. # # NOTE: Path separator in patterns needs to be always `/`. The returned file names use system-specific path separators. - def self.[](*patterns) : Array(String) + def self.[](*patterns : Path | String) : Array(String) glob(patterns) end # :ditto: - def self.[](patterns : Enumerable(String)) : Array(String) + def self.[](patterns : Enumerable) : Array(String) glob(patterns) end @@ -20,12 +20,12 @@ class Dir # If *match_hidden* is `true` the pattern will match hidden files and folders. # # NOTE: Path separator in patterns needs to be always `/`. The returned file names use system-specific path separators. - def self.glob(*patterns, match_hidden = false) : Array(String) + def self.glob(*patterns : Path | String, match_hidden = false) : Array(String) glob(patterns, match_hidden: match_hidden) end # :ditto: - def self.glob(patterns : Enumerable(String), match_hidden = false) : Array(String) + def self.glob(patterns : Enumerable, match_hidden = false) : Array(String) paths = [] of String glob(patterns, match_hidden: match_hidden) do |path| paths << path @@ -40,14 +40,14 @@ class Dir # If *match_hidden* is `true` the pattern will match hidden files and folders. # # NOTE: Path separator in patterns needs to be always `/`. The returned file names use system-specific path separators. - def self.glob(*patterns, match_hidden = false, &block : String -> _) + def self.glob(*patterns : Path | String, match_hidden = false, &block : String -> _) glob(patterns, match_hidden: match_hidden) do |path| yield path end end # :ditto: - def self.glob(patterns : Enumerable(String), match_hidden = false, &block : String -> _) + def self.glob(patterns : Enumerable, match_hidden = false, &block : String -> _) Globber.glob(patterns, match_hidden: match_hidden) do |path| yield path end @@ -72,8 +72,11 @@ class Dir end alias PatternType = DirectoriesOnly | ConstantEntry | EntryMatch | RecursiveDirectories | ConstantDirectory | RootDirectory | DirectoryMatch - def self.glob(patterns : Enumerable(String), **options, &block : String -> _) + def self.glob(patterns : Enumerable, **options, &block : String -> _) patterns.each do |pattern| + if pattern.is_a?(Path) + pattern = pattern.to_posix.to_s + end sequences = compile(pattern) sequences.each do |sequence| diff --git a/src/docs_main.cr b/src/docs_main.cr index d65a12d47046..58a335520065 100644 --- a/src/docs_main.cr +++ b/src/docs_main.cr @@ -17,7 +17,6 @@ require "./http/**" require "./io/**" require "./json" require "./llvm" -require "./logger" require "./macros" require "./math/**" require "./oauth" @@ -32,17 +31,13 @@ require "./weak_ref" require "./xml" require "./yaml" require "./benchmark" -require "./adler32" require "./array" require "./bit_array" require "./box" require "./colorize" require "./complex" -require "./crc32" require "./deque" require "./file_utils" -require "./flate" -require "./gzip" require "./ini" require "./levenshtein" require "./log" @@ -58,7 +53,5 @@ require "./unicode/unicode" require "./uri" require "./uuid" require "./uuid/json" -require "./zip" -require "./zlib" require "./system/*" require "./docs_pseudo_methods" diff --git a/src/docs_pseudo_methods.cr b/src/docs_pseudo_methods.cr index 6a9b44253d7b..5dcbec4ebde0 100644 --- a/src/docs_pseudo_methods.cr +++ b/src/docs_pseudo_methods.cr @@ -50,7 +50,7 @@ end # Returns the instance size of the given class as number of bytes. # -# *type* must be a constant or `typeof()` expresion. It cannot be evaluated at runtime. +# *type* must be a constant or `typeof()` expression. It cannot be evaluated at runtime. # # ``` # instance_sizeof(String) # => 16 diff --git a/src/empty.cr b/src/empty.cr index 8a78751e0f85..35f988afb583 100644 --- a/src/empty.cr +++ b/src/empty.cr @@ -1,5 +1,8 @@ require "primitives" +{% if flag?(:win32) %} + @[Link("libcmt")] # For `mainCRTStartup` +{% end %} lib LibCrystalMain @[Raises] fun __crystal_main(argc : Int32, argv : UInt8**) diff --git a/src/enumerable.cr b/src/enumerable.cr index f2be7b2a4117..f80e23b6cb25 100644 --- a/src/enumerable.cr +++ b/src/enumerable.cr @@ -535,17 +535,6 @@ module Enumerable(T) ary end - # Returns an `Array` with all the elements in the collection that - # match the `RegExp` *pattern*. - # - # ``` - # ["Alice", "Bob"].grep(/^A/) # => ["Alice"] - # ``` - @[Deprecated("Use `#select` instead")] - def grep(pattern) - self.select { |elem| pattern === elem } - end - # Returns a `Hash` whose keys are each different value that the passed block # returned when run for each element in the collection, and which values are # an `Array` of the elements for which the block returned that value. @@ -723,7 +712,7 @@ module Enumerable(T) # ``` def join(separator = "") String.build do |io| - join separator, io + join io, separator end end @@ -733,9 +722,9 @@ module Enumerable(T) # ``` # [1, 2, 3, 4, 5].join(", ") { |i| -i } # => "-1, -2, -3, -4, -5" # ``` - def join(separator = "") + def join(separator = "", & : T ->) String.build do |io| - join(separator, io) do |elem| + join(io, separator) do |elem| io << yield elem end end @@ -744,7 +733,7 @@ module Enumerable(T) # Prints to *io* all the elements in the collection, separated by *separator*. # # ``` - # [1, 2, 3, 4, 5].join(", ", STDOUT) + # [1, 2, 3, 4, 5].join(STDOUT, ", ") # ``` # # Prints: @@ -752,17 +741,33 @@ module Enumerable(T) # ```text # 1, 2, 3, 4, 5 # ``` - def join(separator, io) - join(separator, io) do |elem| + def join(io : IO, separator = "") + join(io, separator) do |elem| elem.to_s(io) end end + # Prints to *io* all the elements in the collection, separated by *separator*. + # + # ``` + # [1, 2, 3, 4, 5].join(STDOUT, ", ") + # ``` + # + # Prints: + # + # ```text + # 1, 2, 3, 4, 5 + # ``` + @[Deprecated(%(Use `#join(io : IO, separator = "") instead`))] + def join(separator, io : IO) + join(io, separator) + end + # Prints to *io* the concatenation of the elements, with the possibility of # controlling how the printing is done via a block. # # ``` - # [1, 2, 3, 4, 5].join(", ", STDOUT) { |i, io| io << "(#{i})" } + # [1, 2, 3, 4, 5].join(STDOUT, ", ") { |i, io| io << "(#{i})" } # ``` # # Prints: @@ -770,13 +775,32 @@ module Enumerable(T) # ```text # (1), (2), (3), (4), (5) # ``` - def join(separator, io) + def join(io : IO, separator = "", & : T, IO ->) each_with_index do |elem, i| io << separator if i > 0 yield elem, io end end + # Prints to *io* the concatenation of the elements, with the possibility of + # controlling how the printing is done via a block. + # + # ``` + # [1, 2, 3, 4, 5].join(STDOUT, ", ") { |i, io| io << "(#{i})" } + # ``` + # + # Prints: + # + # ```text + # (1), (2), (3), (4), (5) + # ``` + @[Deprecated(%(Use `#join(io : IO, separator = "", & : T, IO ->) instead`))] + def join(separator, io : IO) + join(io, separator) do |elem, io| + yield elem, io + end + end + # Returns an `Array` with the results of running the block against each element of the collection. # # ``` @@ -1599,6 +1623,7 @@ module Enumerable(T) # a.zip(b, c) # => [{1, 4, 8}, {2, 5, 7}, {3, 6, 6}] # ``` def zip(*others : Indexable | Iterable | Iterator) + size = self.is_a?(Indexable) ? self.size : 0 pairs = Array(typeof(zip(*others) { |e| break e }.not_nil!)).new(size) zip(*others) { |e| pairs << e } pairs @@ -1669,6 +1694,7 @@ module Enumerable(T) # a.zip?(b, c) # => [{1, 4, 8}, {2, 5, 7}, {3, nil, nil}] # ``` def zip?(*others : Indexable | Iterable | Iterator) + size = self.is_a?(Indexable) ? self.size : 0 pairs = Array(typeof(zip?(*others) { |e| break e }.not_nil!)).new(size) zip?(*others) { |e| pairs << e } pairs diff --git a/src/errno.cr b/src/errno.cr index d5d4b291eddf..7ee985ab794e 100644 --- a/src/errno.cr +++ b/src/errno.cr @@ -6,7 +6,7 @@ lib LibC fun __errno_location : Int* {% elsif flag?(:darwin) || flag?(:freebsd) %} fun __error : Int* - {% elsif flag?(:openbsd) %} + {% elsif flag?(:netbsd) || flag?(:openbsd) %} fun __error = __errno : Int* {% elsif flag?(:win32) %} fun _get_errno(value : Int*) : ErrnoT @@ -43,7 +43,7 @@ enum Errno def self.value : self {% if flag?(:linux) || flag?(:dragonfly) %} Errno.new LibC.__errno_location.value - {% elsif flag?(:darwin) || flag?(:freebsd) || flag?(:openbsd) %} + {% elsif flag?(:darwin) || flag?(:bsd) %} Errno.new LibC.__error.value {% elsif flag?(:win32) %} ret = LibC._get_errno(out errno) @@ -56,7 +56,7 @@ enum Errno def self.value=(errno : Errno) {% if flag?(:linux) || flag?(:dragonfly) %} LibC.__errno_location.value = errno.value - {% elsif flag?(:darwin) || flag?(:freebsd) || flag?(:openbsd) %} + {% elsif flag?(:darwin) || flag?(:bsd) %} LibC.__error.value = errno.value {% elsif flag?(:win32) %} ret = LibC._set_errno(errno.value) diff --git a/src/exception/call_stack.cr b/src/exception/call_stack.cr index 327e79eb4a42..2d6d589faba8 100644 --- a/src/exception/call_stack.cr +++ b/src/exception/call_stack.cr @@ -5,18 +5,10 @@ require "c/stdio" require "c/string" require "./lib_unwind" -{% if flag?(:darwin) %} - require "crystal/mach_o" - require "crystal/dwarf" - - lib LibC - fun _dyld_image_count : UInt32 - fun _dyld_get_image_name(image_index : UInt32) : Char* - fun _dyld_get_image_vmaddr_slide(image_index : UInt32) : Long - end -{% elsif flag?(:bsd) || flag?(:linux) %} - require "crystal/elf" - require "crystal/dwarf" +{% if flag?(:darwin) || flag?(:bsd) || flag?(:linux) %} + require "./call_stack/dwarf" +{% else %} + require "./call_stack/null" {% end %} # Returns the current execution stack as an array containing strings @@ -131,6 +123,21 @@ struct Exception::CallStack end private def self.print_frame(repeated_frame) + {% if flag?(:debug) %} + if @@dwarf_loaded && + (name = decode_function_name(repeated_frame.ip.address)) + file, line, column = Exception::CallStack.decode_line_number(repeated_frame.ip.address) + if file && file != "??" + if repeated_frame.count == 0 + Crystal::System.print_error "[0x%lx] %s at %s:%ld:%i\n", repeated_frame.ip, name, file, line, column + else + Crystal::System.print_error "[0x%lx] %s at %s:%ld:%i (%ld times)\n", repeated_frame.ip, name, file, line, column, repeated_frame.count + 1 + end + return + end + end + {% end %} + frame = decode_frame(repeated_frame.ip) if frame offset, sname = frame @@ -197,224 +204,6 @@ struct Exception::CallStack end end - {% if flag?(:darwin) || flag?(:bsd) || flag?(:linux) %} - @@dwarf_line_numbers : Crystal::DWARF::LineNumbers? - @@dwarf_function_names : Array(Tuple(LibC::SizeT, LibC::SizeT, String))? - - protected def self.decode_line_number(pc) - read_dwarf_sections unless @@dwarf_line_numbers - if ln = @@dwarf_line_numbers - if row = ln.find(pc) - path = "#{row.directory}/#{row.file}" - return {path, row.line, row.column} - end - end - {"??", 0, 0} - end - - protected def self.decode_function_name(pc) - read_dwarf_sections unless @@dwarf_function_names - if fn = @@dwarf_function_names - fn.each do |(low_pc, high_pc, function_name)| - return function_name if low_pc <= pc <= high_pc - end - end - end - - protected def self.parse_function_names_from_dwarf(info, strings) - info.each do |code, abbrev, attributes| - next unless abbrev && abbrev.tag.subprogram? - name = low_pc = high_pc = nil - - attributes.each do |(at, form, value)| - case at - when Crystal::DWARF::AT::DW_AT_name - value = strings.try(&.decode(value.as(UInt32 | UInt64))) if form.strp? - name = value.as(String) - when Crystal::DWARF::AT::DW_AT_low_pc - low_pc = value.as(LibC::SizeT) - when Crystal::DWARF::AT::DW_AT_high_pc - if form.addr? - high_pc = value.as(LibC::SizeT) - elsif value.responds_to?(:to_i) - high_pc = low_pc.as(LibC::SizeT) + value.to_i - end - else - # Not an attribute we care - end - end - - if low_pc && high_pc && name - yield low_pc, high_pc, name - end - end - end - - {% if flag?(:darwin) %} - @@image_slide : LibC::Long? - - protected def self.read_dwarf_sections - locate_dsym_bundle do |mach_o| - mach_o.read_section?("__debug_line") do |sh, io| - @@dwarf_line_numbers = Crystal::DWARF::LineNumbers.new(io, sh.size) - end - - strings = mach_o.read_section?("__debug_str") do |sh, io| - Crystal::DWARF::Strings.new(io, sh.offset, sh.size) - end - - mach_o.read_section?("__debug_info") do |sh, io| - names = [] of {LibC::SizeT, LibC::SizeT, String} - - while (offset = io.pos - sh.offset) < sh.size - info = Crystal::DWARF::Info.new(io, offset) - - mach_o.read_section?("__debug_abbrev") do |sh, io| - info.read_abbreviations(io) - end - - parse_function_names_from_dwarf(info, strings) do |low_pc, high_pc, name| - names << {low_pc, high_pc, name} - end - end - - @@dwarf_function_names = names - end - end - end - - # DWARF uses fixed addresses but Darwin loads exectutables at a random - # address, so we must remove the load offset from the IP to match the - # addresses in DWARF sections. - # - # See https://en.wikipedia.org/wiki/Address_space_layout_randomization - protected def self.decode_address(ip) - ip.address - image_slide - end - - # Searches the companion dSYM bundle with the DWARF sections for the - # current program as generated by `dsymutil`. It may be a `foo.dwarf` file - # or within a `foo.dSYM` bundle for a program named `foo`. - # - # See for details. - private def self.locate_dsym_bundle - program = Process.executable_path - return unless program - - files = { - "#{program}.dSYM/Contents/Resources/DWARF/#{File.basename(program)}", - "#{program}.dwarf" - } - - files.each do |dwarf| - next unless File.exists?(dwarf) - - Crystal::MachO.open(program) do |mach_o| - Crystal::MachO.open(dwarf) do |dsym| - if dsym.uuid == mach_o.uuid - return yield dsym - end - end - end - end - - nil - end - - # The address offset at which the program was loaded at. - private def self.image_slide - @@image_slide ||= search_image_slide - end - - private def self.search_image_slide - buffer = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*) - size = LibC::PATH_MAX.to_u32 - - if LibC._NSGetExecutablePath(buffer, pointerof(size)) == -1 - buffer = GC.malloc_atomic(size).as(UInt8*) - if LibC._NSGetExecutablePath(buffer, pointerof(size)) == -1 - return LibC::Long.new(0) - end - end - - program = String.new(buffer) - - LibC._dyld_image_count.times do |i| - if program == String.new(LibC._dyld_get_image_name(i)) - return LibC._dyld_get_image_vmaddr_slide(i) - end - end - - LibC::Long.new(0) - end - {% else %} - @@base_address : UInt64|UInt32|Nil - - protected def self.read_dwarf_sections - program = Process.executable_path - return unless program && File.readable? program - Crystal::ELF.open(program) do |elf| - elf.read_section?(".text") do |sh, _| - @@base_address = sh.addr - sh.offset - end - - elf.read_section?(".debug_line") do |sh, io| - @@dwarf_line_numbers = Crystal::DWARF::LineNumbers.new(io, sh.size) - end - - strings = elf.read_section?(".debug_str") do |sh, io| - Crystal::DWARF::Strings.new(io, sh.offset, sh.size) - end - - elf.read_section?(".debug_info") do |sh, io| - names = [] of {LibC::SizeT, LibC::SizeT, String} - - while (offset = io.pos - sh.offset) < sh.size - info = Crystal::DWARF::Info.new(io, offset) - - elf.read_section?(".debug_abbrev") do |sh, io| - info.read_abbreviations(io) - end - - parse_function_names_from_dwarf(info, strings) do |name, low_pc, high_pc| - names << {name, low_pc, high_pc} - end - end - - @@dwarf_function_names = names - end - end - end - - # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux - # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load - # executables at a random address, so we must remove the load offset from - # the IP to match the addresses in DWARF sections. - # - # See https://en.wikipedia.org/wiki/Address_space_layout_randomization - protected def self.decode_address(ip) - if LibC.dladdr(ip, out info) != 0 - unless info.dli_fbase.address == @@base_address - return ip.address - info.dli_fbase.address - end - end - ip.address - end - {% end %} - {% else %} - def self.decode_address(ip) - ip - end - - def self.decode_line_number(pc) - {"??", 0, 0} - end - - def self.decode_function_name(pc) - nil - end - {% end %} - protected def self.decode_frame(ip, original_ip = ip) if LibC.dladdr(ip, out info) != 0 offset = original_ip - info.dli_saddr @@ -428,4 +217,10 @@ struct Exception::CallStack end end end + + {% if flag?(:debug) %} + # load dwarf on start up of the program when compiled with --debug + # this will make dwarf available on print_frame that is used on __crystal_sigfault_handler + load_dwarf + {% end %} end diff --git a/src/exception/call_stack/dwarf.cr b/src/exception/call_stack/dwarf.cr new file mode 100644 index 000000000000..8e2a95c7a2cb --- /dev/null +++ b/src/exception/call_stack/dwarf.cr @@ -0,0 +1,61 @@ +require "crystal/dwarf" +{% if flag?(:darwin) %} + require "./mach_o" +{% else %} + require "./elf" +{% end %} + +struct Exception::CallStack + @@dwarf_loaded = false + @@dwarf_line_numbers : Crystal::DWARF::LineNumbers? + @@dwarf_function_names : Array(Tuple(LibC::SizeT, LibC::SizeT, String))? + + protected def self.decode_line_number(pc) + load_dwarf unless @@dwarf_loaded + if ln = @@dwarf_line_numbers + if row = ln.find(pc) + path = "#{row.directory}/#{row.file}" + return {path, row.line, row.column} + end + end + {"??", 0, 0} + end + + protected def self.decode_function_name(pc) + load_dwarf unless @@dwarf_loaded + if fn = @@dwarf_function_names + fn.each do |(low_pc, high_pc, function_name)| + return function_name if low_pc <= pc <= high_pc + end + end + end + + protected def self.parse_function_names_from_dwarf(info, strings) + info.each do |code, abbrev, attributes| + next unless abbrev && abbrev.tag.subprogram? + name = low_pc = high_pc = nil + + attributes.each do |(at, form, value)| + case at + when Crystal::DWARF::AT::DW_AT_name + value = strings.try(&.decode(value.as(UInt32 | UInt64))) if form.strp? + name = value.as(String) + when Crystal::DWARF::AT::DW_AT_low_pc + low_pc = value.as(LibC::SizeT) + when Crystal::DWARF::AT::DW_AT_high_pc + if form.addr? + high_pc = value.as(LibC::SizeT) + elsif value.responds_to?(:to_i) + high_pc = low_pc.as(LibC::SizeT) + value.to_i + end + else + # Not an attribute we care + end + end + + if low_pc && high_pc && name + yield low_pc, high_pc, name + end + end + end +end diff --git a/src/exception/call_stack/elf.cr b/src/exception/call_stack/elf.cr new file mode 100644 index 000000000000..865b70029540 --- /dev/null +++ b/src/exception/call_stack/elf.cr @@ -0,0 +1,51 @@ +require "crystal/elf" +require "c/link" + +struct Exception::CallStack + protected def self.load_dwarf + phdr_callback = LibC::DlPhdrCallback.new do |info, size, data| + # The first entry is the header for the current program + read_dwarf_sections(info.value.addr) + 1 + end + + LibC.dl_iterate_phdr(phdr_callback, nil) + @@dwarf_loaded = true + end + + protected def self.read_dwarf_sections(base_address = 0) + program = Process.executable_path + return unless program && File.readable? program + Crystal::ELF.open(program) do |elf| + elf.read_section?(".debug_line") do |sh, io| + @@dwarf_line_numbers = Crystal::DWARF::LineNumbers.new(io, sh.size, base_address) + end + + strings = elf.read_section?(".debug_str") do |sh, io| + Crystal::DWARF::Strings.new(io, sh.offset, sh.size) + end + + elf.read_section?(".debug_info") do |sh, io| + names = [] of {LibC::SizeT, LibC::SizeT, String} + + while (offset = io.pos - sh.offset) < sh.size + info = Crystal::DWARF::Info.new(io, offset) + + elf.read_section?(".debug_abbrev") do |sh, io| + info.read_abbreviations(io) + end + + parse_function_names_from_dwarf(info, strings) do |low_pc, high_pc, name| + names << {low_pc + base_address, high_pc + base_address, name} + end + end + + @@dwarf_function_names = names + end + end + end + + protected def self.decode_address(ip) + ip.address + end +end diff --git a/src/exception/call_stack/mach_o.cr b/src/exception/call_stack/mach_o.cr new file mode 100644 index 000000000000..b858fe9da691 --- /dev/null +++ b/src/exception/call_stack/mach_o.cr @@ -0,0 +1,111 @@ +require "crystal/mach_o" + +lib LibC + fun _dyld_image_count : UInt32 + fun _dyld_get_image_name(image_index : UInt32) : Char* + fun _dyld_get_image_vmaddr_slide(image_index : UInt32) : Long +end + +struct Exception::CallStack + @@image_slide : LibC::Long? + + protected def self.load_dwarf + read_dwarf_sections + @@dwarf_loaded = true + end + + protected def self.read_dwarf_sections + locate_dsym_bundle do |mach_o| + mach_o.read_section?("__debug_line") do |sh, io| + @@dwarf_line_numbers = Crystal::DWARF::LineNumbers.new(io, sh.size) + end + + strings = mach_o.read_section?("__debug_str") do |sh, io| + Crystal::DWARF::Strings.new(io, sh.offset, sh.size) + end + + mach_o.read_section?("__debug_info") do |sh, io| + names = [] of {LibC::SizeT, LibC::SizeT, String} + + while (offset = io.pos - sh.offset) < sh.size + info = Crystal::DWARF::Info.new(io, offset) + + mach_o.read_section?("__debug_abbrev") do |sh, io| + info.read_abbreviations(io) + end + + parse_function_names_from_dwarf(info, strings) do |low_pc, high_pc, name| + names << {low_pc, high_pc, name} + end + end + + @@dwarf_function_names = names + end + end + end + + # DWARF uses fixed addresses but Darwin loads executables at a random + # address, so we must remove the load offset from the IP to match the + # addresses in DWARF sections. + # + # See https://en.wikipedia.org/wiki/Address_space_layout_randomization + protected def self.decode_address(ip) + ip.address - image_slide + end + + # Searches the companion dSYM bundle with the DWARF sections for the + # current program as generated by `dsymutil`. It may be a `foo.dwarf` file + # or within a `foo.dSYM` bundle for a program named `foo`. + # + # See for details. + private def self.locate_dsym_bundle + program = Process.executable_path + return unless program + + files = { + "#{program}.dSYM/Contents/Resources/DWARF/#{File.basename(program)}", + "#{program}.dwarf", + } + + files.each do |dwarf| + next unless File.exists?(dwarf) + + Crystal::MachO.open(program) do |mach_o| + Crystal::MachO.open(dwarf) do |dsym| + if dsym.uuid == mach_o.uuid + return yield dsym + end + end + end + end + + nil + end + + # The address offset at which the program was loaded at. + private def self.image_slide + @@image_slide ||= search_image_slide + end + + private def self.search_image_slide + buffer = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*) + size = LibC::PATH_MAX.to_u32 + + if LibC._NSGetExecutablePath(buffer, pointerof(size)) == -1 + buffer = GC.malloc_atomic(size).as(UInt8*) + if LibC._NSGetExecutablePath(buffer, pointerof(size)) == -1 + return LibC::Long.new(0) + end + end + + program = String.new(buffer) + + LibC._dyld_image_count.times do |i| + if program == String.new(LibC._dyld_get_image_name(i)) + return LibC._dyld_get_image_vmaddr_slide(i) + end + end + + LibC::Long.new(0) + end +end diff --git a/src/exception/call_stack/null.cr b/src/exception/call_stack/null.cr new file mode 100644 index 000000000000..6b9d2ab6820f --- /dev/null +++ b/src/exception/call_stack/null.cr @@ -0,0 +1,13 @@ +struct Exception::CallStack + def self.decode_address(ip) + ip + end + + def self.decode_line_number(pc) + {"??", 0, 0} + end + + def self.decode_function_name(pc) + nil + end +end diff --git a/src/fiber.cr b/src/fiber.cr index 675352e14574..4bb622300402 100644 --- a/src/fiber.cr +++ b/src/fiber.cr @@ -273,7 +273,7 @@ class Fiber def to_s(io : IO) : Nil io << "#<" << self.class.name << ":0x" - object_id.to_s(16, io) + object_id.to_s(io, 16) if name = @name io << ": " << name end diff --git a/src/file.cr b/src/file.cr index 095da994e7f5..2ce83afc457e 100644 --- a/src/file.cr +++ b/src/file.cr @@ -168,6 +168,24 @@ class File < IO::FileDescriptor info(path1.to_s, follow_symlinks).same_file? info(path2.to_s, follow_symlinks) end + # Compares two files *filename1* to *filename2* to determine if they are identical. + # Returns `true` if content are the same, `false` otherwise. + # + # ``` + # File.write("file.cr", "1") + # File.write("bar.cr", "1") + # File.same_content?("file.cr", "bar.cr") # => true + # ``` + def self.same_content?(path1 : Path | String, path2 : Path | String) : Bool + open(path1, "rb") do |file1| + open(path2, "rb") do |file2| + return false unless file1.size == file2.size + + same_content?(file1, file2) + end + end + end + # Returns the size of the file at *filename* in bytes. # Raises `File::NotFoundError` if the file at *filename* does not exist. # @@ -701,6 +719,26 @@ class File < IO::FileDescriptor end end + # Copies the file *src* to the file *dst*. + # Permission bits are copied too. + # + # ``` + # File.chmod("afile", 0o600) + # File.copy("afile", "afile_copy") + # File.info("afile_copy").permissions.value # => 0o600 + # ``` + def self.copy(src : String | Path, dst : String | Path) + open(src) do |s| + open(dst, "wb") do |d| + # TODO use sendfile or copy_file_range syscall. See #8926, #8919 + IO.copy(s, d) + end + + # Set the permissions after the content is written in case src permissions is read-only + chmod(dst, s.info.permissions) + end + end + # Returns a new string formed by joining the strings using `File::SEPARATOR`. # # ``` @@ -771,6 +809,9 @@ class File < IO::FileDescriptor # then the syscall *fdatasync* will be used and only data required for # subsequent data retrieval is flushed. Metadata such as modified time and # access time is not written. + # + # NOTE: Metadata is flushed even when *flush_metadata* is false on Windows + # and DragonFly BSD. def fsync(flush_metadata = true) : Nil flush system_fsync(flush_metadata) diff --git a/src/file_utils.cr b/src/file_utils.cr index 5885ef0fd368..41fde0695edd 100644 --- a/src/file_utils.cr +++ b/src/file_utils.cr @@ -38,39 +38,10 @@ module FileUtils # File.write("bar.cr", "1") # FileUtils.cmp("file.cr", "bar.cr") # => true # ``` - def cmp(filename1 : String, filename2 : String) - return false unless File.size(filename1) == File.size(filename2) - - File.open(filename1, "rb") do |file1| - File.open(filename2, "rb") do |file2| - cmp(file1, file2) - end - end - end - - # Compares two streams *stream1* to *stream2* to determine if they are identical. - # Returns `true` if content are the same, `false` otherwise. # - # ``` - # require "file_utils" - # - # File.write("afile", "123") - # stream1 = File.open("afile") - # stream2 = IO::Memory.new("123") - # FileUtils.cmp(stream1, stream2) # => true - # ``` - def cmp(stream1 : IO, stream2 : IO) - buf1 = uninitialized UInt8[1024] - buf2 = uninitialized UInt8[1024] - - while true - read1 = stream1.read(buf1.to_slice) - read2 = stream2.read_fully?(buf2.to_slice[0, read1]) - return false unless read2 - - return false if buf1.to_unsafe.memcmp(buf2.to_unsafe, read1) != 0 - return true if read1 == 0 - end + # NOTE: Alias of `File.same_content?` + def cmp(filename1 : String, filename2 : String) + File.same_content?(filename1, filename2) end # Attempts to set the access and modification times of the file named @@ -117,12 +88,8 @@ module FileUtils # File.info("afile_copy").permissions.value # => 0o600 # ``` def cp(src_path : String, dest : String) - File.open(src_path) do |s| - dest += File::SEPARATOR + File.basename(src_path) if Dir.exists?(dest) - File.open(dest, "wb", s.info.permissions) do |d| - IO.copy(s, d) - end - end + dest += File::SEPARATOR + File.basename(src_path) if Dir.exists?(dest) + File.copy(src_path, dest) end # Copies a list of files *src* to *dest*. @@ -151,7 +118,7 @@ module FileUtils # ``` def cp_r(src_path : String, dest_path : String) if Dir.exists?(src_path) - Dir.mkdir(dest_path) + Dir.mkdir(dest_path) unless Dir.exists?(dest_path) Dir.each_child(src_path) do |entry| src = File.join(src_path, entry) dest = File.join(dest_path, entry) @@ -414,7 +381,7 @@ module FileUtils src = File.join(path, entry) rm_r(src) end - Dir.rmdir(path) + Dir.delete(path) else File.delete(path) end @@ -480,7 +447,7 @@ module FileUtils # # NOTE: Alias of `Dir.rmdir` def rmdir(path : String) : Nil - Dir.rmdir(path) + Dir.delete(path) end # Removes all directories at the given *paths*. @@ -492,7 +459,7 @@ module FileUtils # ``` def rmdir(paths : Enumerable(String)) : Nil paths.each do |path| - Dir.rmdir(path) + Dir.delete(path) end end end diff --git a/src/flate.cr b/src/flate.cr deleted file mode 100644 index 1f69693c26cd..000000000000 --- a/src/flate.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "compress/deflate" - -{% puts "Warning: Flate is deprecated, use Compress::Deflate" %} - -# DEPRECATED: Use `Compress::Deflate` -alias Flate = Compress::Deflate diff --git a/src/float/printer/grisu3.cr b/src/float/printer/grisu3.cr index 4b6bb151314f..79e0a1a67386 100644 --- a/src/float/printer/grisu3.cr +++ b/src/float/printer/grisu3.cr @@ -341,7 +341,7 @@ module Float::Printer::Grisu3 # the difference between w and boundary_minus/plus (a power of 2) and to # compute scaled_boundary_minus/plus by subtracting/adding from # scaled_w. However the code becomes much less readable and the speed - # enhancements are not terriffic. + # enhancements are not terrific. scaled_boundary_minus = boundaries[:minus] * ten_mk scaled_boundary_plus = boundaries[:plus] * ten_mk diff --git a/src/gc/boehm.cr b/src/gc/boehm.cr index b72ec95bd44a..1da7a4af7199 100644 --- a/src/gc/boehm.cr +++ b/src/gc/boehm.cr @@ -9,7 +9,7 @@ {% if flag?(:freebsd) || flag?(:dragonfly) %} @[Link("gc-threaded")] {% else %} - @[Link("gc", static: true)] + @[Link("gc")] {% end %} lib LibGC diff --git a/src/gc/none.cr b/src/gc/none.cr index 1c1690a0e368..4f5cf5f7f7e3 100644 --- a/src/gc/none.cr +++ b/src/gc/none.cr @@ -43,6 +43,9 @@ module GC def self.add_finalizer(object) end + def self.register_disappearing_link(pointer : Void**) + end + def self.stats zero = LibC::ULong.new(0) Stats.new(zero, zero, zero, zero, zero) diff --git a/src/gzip.cr b/src/gzip.cr deleted file mode 100644 index 74e6210f0060..000000000000 --- a/src/gzip.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "compress/gzip" - -{% puts "Warning: Gzip is deprecated, use Compress::Gzip" %} - -# DEPRECATED: Use `Compress::Gzip` -alias Gzip = Compress::Gzip diff --git a/src/hash.cr b/src/hash.cr index 98bf0a796916..af982de29d66 100644 --- a/src/hash.cr +++ b/src/hash.cr @@ -140,7 +140,7 @@ class Hash(K, V) # shift of the buffer (expensive). # # There might be other optimizations to try out, like not using Linear Probing, - # but for now this implementaton is much faster than the old one which used + # but for now this implementation is much faster than the old one which used # linked lists (closed addressing). # # All methods that deal with this implementation come after the constructors. @@ -1255,7 +1255,7 @@ class Hash(K, V) # ``` # # The enumeration follows the order the keys were inserted. - def each(& : Tuple(K, V) ->) : Nil + def each : Nil each_entry_with_index do |entry, i| yield({entry.key, entry.value}) end diff --git a/src/http.cr b/src/http.cr index facbdd2f897c..33cbc7895b39 100644 --- a/src/http.cr +++ b/src/http.cr @@ -2,6 +2,7 @@ require "uri" {% unless flag?(:win32) %} require "./http/client" require "./http/server" + require "./http/log" {% end %} require "./http/common" diff --git a/src/http/client.cr b/src/http/client.cr index 4bcf8f4e0526..822a63e43de4 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -105,21 +105,21 @@ class HTTP::Client # ``` {% if flag?(:without_openssl) %} getter! tls : Nil - @socket : TCPSocket | Nil alias TLSContext = Bool | Nil {% else %} getter! tls : OpenSSL::SSL::Context::Client - @socket : TCPSocket | OpenSSL::SSL::Socket | Nil alias TLSContext = OpenSSL::SSL::Context::Client | Bool | Nil {% end %} # Whether automatic compression/decompression is enabled. property? compress : Bool = true + @io : IO? @dns_timeout : Float64? @connect_timeout : Float64? @read_timeout : Float64? @write_timeout : Float64? + @reconnect = true # Creates a new HTTP client with the given *host*, *port* and *tls* # configurations. If no port is given, the default one will @@ -148,6 +148,16 @@ class HTTP::Client @port = (port || (@tls ? 443 : 80)).to_i end + # Creates a new HTTP client bound to an existing `IO`. + # *host* and *port* can be specified and they will be used + # to conform the `Host` header on each request. + # Instances created with this constructor cannot be reconnected. Once + # `close` is called explicitly or if the connection doesn't support keep-alive, + # the next call to make a request will raise an exception. + def initialize(@io : IO, @host = "", @port = 80) + @reconnect = false + end + private def check_host_only(string : String) # When parsing a URI with just a host # we end up with a URI with just a path @@ -567,7 +577,9 @@ class HTTP::Client # response.body # => "..." # ``` def exec(request : HTTP::Request) : HTTP::Client::Response - exec_internal(request) + around_exec(request) do + exec_internal(request) + end end private def exec_internal(request) @@ -585,7 +597,7 @@ class HTTP::Client private def exec_internal_single(request) decompress = send_request(request) - HTTP::Client::Response.from_io?(socket, ignore_body: request.ignore_body?, decompress: decompress) + HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?, decompress: decompress) end private def handle_response(response) @@ -605,8 +617,10 @@ class HTTP::Client # end # ``` def exec(request : HTTP::Request, &block) - exec_internal(request) do |response| - yield response + around_exec(request) do + exec_internal(request) do |response| + yield response + end end end @@ -632,7 +646,7 @@ class HTTP::Client private def exec_internal_single(request) decompress = send_request(request) - HTTP::Client::Response.from_io?(socket, ignore_body: request.ignore_body?, decompress: decompress) do |response| + HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?, decompress: decompress) do |response| yield response end end @@ -647,8 +661,8 @@ class HTTP::Client private def send_request(request) decompress = set_defaults request run_before_request_callbacks(request) - request.to_io(socket) - socket.flush + request.to_io(io) + io.flush decompress end @@ -746,29 +760,32 @@ class HTTP::Client # Closes this client. If used again, a new connection will be opened. def close - @socket.try &.close - @socket = nil + @io.try &.close + @io = nil end private def new_request(method, path, headers, body : BodyType) HTTP::Request.new(method, path, headers, body) end - private def socket - socket = @socket - return socket if socket + private def io + io = @io + return io if io + unless @reconnect + raise "This HTTP::Client cannot be reconnected" + end hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host - socket = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout - socket.read_timeout = @read_timeout if @read_timeout - socket.write_timeout = @write_timeout if @write_timeout - socket.sync = false + io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout + io.read_timeout = @read_timeout if @read_timeout + io.write_timeout = @write_timeout if @write_timeout + io.sync = false {% if !flag?(:without_openssl) %} if tls = @tls - tcp_socket = socket + tcp_socket = io begin - socket = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host) + io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host) rescue exc # don't leak the TCP socket when the SSL connection failed tcp_socket.close @@ -777,7 +794,7 @@ class HTTP::Client end {% end %} - @socket = socket + @io = io end private def host_header @@ -849,6 +866,40 @@ class HTTP::Client yield client, path end end + + # This method is called when executing the request. Although it can be + # redefined, it is recommended to use the `def_around_exec` macro to be + # able to add new behaviors without loosing prior existing ones. + protected def around_exec(request) + yield + end + + # This macro allows injecting code to be run before and after the execution + # of the request. It should return the yielded value. It must be called with 1 + # block argument that will be used to pass the `HTTP::Request`. + # + # ``` + # class HTTP::Client + # def_around_exec do |request| + # # do something before exec + # res = yield + # # do something after exec + # res + # end + # end + # ``` + macro def_around_exec(&block) + protected def around_exec(%request) + previous_def do + {% if block.args.size != 1 %} + {% raise "Wrong number of block arguments (given #{block.args.size}, expected: 1)" %} + {% end %} + + {{ block.args.first.id }} = %request + {{ block.body }} + end + end + end end require "socket" diff --git a/src/http/common.cr b/src/http/common.cr index f218c097b6a3..896ba17c751a 100644 --- a/src/http/common.cr +++ b/src/http/common.cr @@ -293,7 +293,7 @@ module HTTP def self.serialize_chunked_body(io, body) buf = uninitialized UInt8[8192] while (buf_length = body.read(buf.to_slice)) > 0 - buf_length.to_s(16, io) + buf_length.to_s(io, 16) io << "\r\n" io.write(buf.to_slice[0, buf_length]) io << "\r\n" diff --git a/src/http/cookie.cr b/src/http/cookie.cr index 53ce31cb0472..e867e68cac2f 100644 --- a/src/http/cookie.cr +++ b/src/http/cookie.cr @@ -5,9 +5,12 @@ module HTTP class Cookie # Possible values for the `SameSite` cookie as described in the [Same-site Cookies Draft](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1.1). enum SameSite + # The browser will send cookies with both cross-site requests and same-site requests. + # + # The `None` directive requires the `secure` attribute to be `true` to mitigate risks associated with cross-site access. + None # Prevents the cookie from being sent by the browser in all cross-site browsing contexts. Strict - # Allows the cookie to be sent by the browser during top-level navigations that use a [safe](https://tools.ietf.org/html/rfc7231#section-4.2.1) HTTP method. Lax end diff --git a/src/http/formdata.cr b/src/http/formdata.cr index 380b5d87052c..ca26eb01c715 100644 --- a/src/http/formdata.cr +++ b/src/http/formdata.cr @@ -77,7 +77,7 @@ require "mime/multipart" # end # ``` module HTTP::FormData - # Parses a multipart/form-data message, yielding a `FormData::Parser`. + # Parses a multipart/form-data message, yielding a `FormData::Part`. # # ``` # require "http" @@ -97,7 +97,7 @@ module HTTP::FormData end end - # Parses a multipart/form-data message, yielding a `FormData::Parser`. + # Parses a multipart/form-data message, yielding a `FormData::Part`. # # ``` # require "http" diff --git a/src/http/formdata/builder.cr b/src/http/formdata/builder.cr index f8d9893ab913..5947dc06724b 100644 --- a/src/http/formdata/builder.cr +++ b/src/http/formdata/builder.cr @@ -91,19 +91,19 @@ module HTTP::FormData if creation_time = metadata.creation_time io << %(; creation-date=") - creation_time.to_s("%a, %d %b %Y %H:%M:%S %z", io) + creation_time.to_s(io, "%a, %d %b %Y %H:%M:%S %z") io << '"' end if modification_time = metadata.modification_time io << %(; modification-date=") - modification_time.to_s("%a, %d %b %Y %H:%M:%S %z", io) + modification_time.to_s(io, "%a, %d %b %Y %H:%M:%S %z") io << '"' end if read_time = metadata.read_time io << %(; read-date=") - read_time.to_s("%a, %d %b %Y %H:%M:%S %z", io) + read_time.to_s(io, "%a, %d %b %Y %H:%M:%S %z") io << '"' end diff --git a/src/http/log.cr b/src/http/log.cr new file mode 100644 index 000000000000..7f16c4c63e90 --- /dev/null +++ b/src/http/log.cr @@ -0,0 +1,19 @@ +require "log" + +class HTTP::Client + Log = ::Log.for(self) + + def_around_exec do |request| + emit_log(request) + yield + end + + protected def emit_log(request) + Log.debug &.emit("Performing request", + method: request.method, + host: host, + port: port, + resource: request.resource, + ) + end +end diff --git a/src/http/params.cr b/src/http/params.cr index 9a5de41f3ecb..e1897bd99234 100644 --- a/src/http/params.cr +++ b/src/http/params.cr @@ -127,9 +127,9 @@ module HTTP # params # => "color=black&name=crystal&year=2012+-+today" # ``` def self.build(&block : Builder ->) : String - form_builder = Builder.new - yield form_builder - form_builder.to_s + String.build do |io| + yield Builder.new(io) + end end protected getter raw_params @@ -146,10 +146,6 @@ module HTTP self.raw_params == other.raw_params end - def ==(other) - false - end - # Returns first value for specified param name. # # ``` @@ -189,14 +185,26 @@ module HTTP # ``` delegate empty?, to: raw_params - # Sets first *value* for specified param *name*. + # Sets the *name* key to *value*. # # ``` - # params["item"] = "pencil" - # ``` - def []=(name, value) - raw_params[name] ||= [""] - raw_params[name][0] = value + # require "http/params" + # + # params = HTTP::Params{"a" => ["b", "c"]} + # params["a"] = "d" + # params["a"] # => "d" + # params.fetch_all("a") # => ["d"] + # + # params["a"] = ["e", "f"] + # params["a"] # => "e" + # params.fetch_all("a") # => ["e", "f"] + # ``` + def []=(name, value : String | Array(String)) + raw_params[name] = + case value + in String then [value] + in Array(String) then value + end end # Returns all values for specified param *name*. @@ -330,10 +338,7 @@ module HTTP # Every parameter added is directly written to an `IO`, # where keys and values are properly escaped. class Builder - @io : IO - @first : Bool - - def initialize(@io = IO::Memory.new) + def initialize(@io : IO) @first = true end @@ -352,10 +357,6 @@ module HTTP values.each { |value| add(key, value) } self end - - def to_s(io : IO) : Nil - io << @io.to_s - end end end end diff --git a/src/http/request.cr b/src/http/request.cr index ade24aeabc56..063ece74d0a4 100644 --- a/src/http/request.cr +++ b/src/http/request.cr @@ -20,14 +20,20 @@ class HTTP::Request @query_params : Params? @uri : URI? - # The network address that sent the request to an HTTP server. - # - # `HTTP::Server` will try to fill this property, and its value - # will have a format like "IP:port", but this format is not guaranteed. - # Middlewares can overwrite this value. - # - # This property is not used by `HTTP::Client`. - property remote_address : String? + {% unless flag?(:win32) %} + # The network address that sent the request to an HTTP server. + # + # `HTTP::Server` will try to fill this property, and its value + # will have a format like "IP:port", but this format is not guaranteed. + # Middlewares can overwrite this value. + # + # This property is not used by `HTTP::Client`. + property remote_address : Socket::Address? + {% else %} + # TODO: Remove this once `Socket` is working on Windows + + property remote_address : Nil + {% end %} def self.new(method : String, resource : String, headers : Headers? = nil, body : String | Bytes | IO | Nil = nil, version = "HTTP/1.1") # Duplicate headers to prevent the request from modifying data that the user might hold. @@ -110,7 +116,7 @@ class HTTP::Request request = new line.method, line.resource, headers, body, line.http_version, internal: nil if io.responds_to?(:remote_address) - request.remote_address = io.remote_address.try &.to_s + request.remote_address = io.remote_address end return request diff --git a/src/http/server.cr b/src/http/server.cr index 8d4d948a41b3..ca92675a673f 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -73,7 +73,7 @@ require "log" # # ## Binding to sockets # -# The server can be bound to one ore more server sockets (see `#bind`) +# The server can be bound to one or more server sockets (see `#bind`) # # Supported types: # @@ -511,6 +511,8 @@ class HTTP::Server {% end %} @processor.process(io, io) + ensure + io.close rescue IO::Error end # This method handles exceptions raised at `Socket#accept?`. diff --git a/src/http/server/handler.cr b/src/http/server/handler.cr index 5bc57bd3cc14..3b1f97113b80 100644 --- a/src/http/server/handler.cr +++ b/src/http/server/handler.cr @@ -2,7 +2,7 @@ require "./context" # A handler is a class which includes `HTTP::Handler` and implements the `call` method. # You can use a handler to intercept any incoming request and can modify the response. -# These can be used for request throttling, ip-based whitelisting, adding custom headers e.g. +# These can be used for request throttling, ip-based filtering, adding custom headers e.g. # # ### A custom handler # diff --git a/src/http/server/handlers/compress_handler.cr b/src/http/server/handlers/compress_handler.cr index d124fd477765..8dd5e66c4404 100644 --- a/src/http/server/handlers/compress_handler.cr +++ b/src/http/server/handlers/compress_handler.cr @@ -12,17 +12,52 @@ class HTTP::CompressHandler {% if flag?(:without_zlib) %} call_next(context) {% else %} - request_headers = context.request.headers - - if request_headers.includes_word?("Accept-Encoding", "gzip") - context.response.headers["Content-Encoding"] = "gzip" - context.response.output = Compress::Gzip::Writer.new(context.response.output, sync_close: true) - elsif request_headers.includes_word?("Accept-Encoding", "deflate") - context.response.headers["Content-Encoding"] = "deflate" - context.response.output = Compress::Deflate::Writer.new(context.response.output, sync_close: true) - end - + context.response.output = CompressIO.new(context.response.output, context) call_next(context) {% end %} end + + {% unless flag?(:without_zlib) %} + private class CompressIO < IO + def initialize(@io : IO, @context : HTTP::Server::Context) + @checked = false + end + + def read(slice : Bytes) + raise NotImplementedError.new("read") + end + + def write(slice : Bytes) : Nil + check_output unless @checked + @io.write(slice) + end + + def flush + @io.flush + end + + def close + @io.close + end + + private def check_output + @checked = true + + return if @context.response.wrote_headers? + return if @context.response.headers.has_key?("Content-Encoding") + + request_headers = @context.request.headers + + if request_headers.includes_word?("Accept-Encoding", "gzip") + @context.response.headers["Content-Encoding"] = "gzip" + @context.response.headers.delete("Content-Length") + @io = Compress::Gzip::Writer.new(@io, sync_close: true) + elsif request_headers.includes_word?("Accept-Encoding", "deflate") + @context.response.headers["Content-Encoding"] = "deflate" + @context.response.headers.delete("Content-Length") + @io = Compress::Deflate::Writer.new(@io, sync_close: true) + end + end + end + {% end %} end diff --git a/src/http/server/handlers/log_handler.cr b/src/http/server/handlers/log_handler.cr index c3fb0af31e8b..43c9af150c3e 100644 --- a/src/http/server/handlers/log_handler.cr +++ b/src/http/server/handlers/log_handler.cr @@ -24,7 +24,22 @@ class HTTP::LogHandler req = context.request res = context.response - @log.info { "#{req.remote_address || "-"} - #{req.method} #{req.resource} #{req.version} - #{res.status_code} (#{elapsed_text})" } + + addr = + {% begin %} + case remote_address = req.remote_address + when nil + "-" + {% unless flag?(:win32) %} + when Socket::IPAddress + remote_address.address + {% end %} + else + remote_address + end + {% end %} + + @log.info { "#{addr} - #{req.method} #{req.resource} #{req.version} - #{res.status_code} (#{elapsed_text})" } end end diff --git a/src/http/server/handlers/static_file_handler.cr b/src/http/server/handlers/static_file_handler.cr index 0416f0cef056..e70b694cb34f 100644 --- a/src/http/server/handlers/static_file_handler.cr +++ b/src/http/server/handlers/static_file_handler.cr @@ -3,7 +3,11 @@ require "html" require "uri" require "mime" -# A simple handler that lists directories and serves files under a given public directory. +# A handler that lists directories and serves files under a given public directory. +# +# This handler can send precompressed content, if the client accepts it, and a file +# with the same name and `.gz` extension appended is found in the same directory. +# Precompressed files are only served if they are newer than the original file. class HTTP::StaticFileHandler include HTTP::Handler @@ -76,6 +80,21 @@ class HTTP::StaticFileHandler end context.response.content_type = MIME.from_filename(file_path.to_s, "application/octet-stream") + + # Checks if pre-gzipped file can be served + if context.request.headers.includes_word?("Accept-Encoding", "gzip") + gz_file_path = "#{file_path}.gz" + + if File.exists?(gz_file_path) && + # Allow small time drift. In some file systems, using `gz --keep` to + # compress the file will keep the modification time of the original file + # but truncating some decimals + last_modified - modification_time(gz_file_path) < 1.millisecond + file_path = gz_file_path + context.response.headers["Content-Encoding"] = "gzip" + end + end + context.response.content_length = File.size(file_path) File.open(file_path) do |file| IO.copy(file, context.response) diff --git a/src/http/server/handlers/websocket_handler.cr b/src/http/server/handlers/websocket_handler.cr index df10143a6381..52813d8c8734 100644 --- a/src/http/server/handlers/websocket_handler.cr +++ b/src/http/server/handlers/websocket_handler.cr @@ -57,8 +57,6 @@ class HTTP::WebSocketHandler ws_session = WebSocket.new(io, sync_close: false) @proc.call(ws_session, context) ws_session.run - ensure - io.close end end diff --git a/src/http/server/request_processor.cr b/src/http/server/request_processor.cr index 05e43e1495b7..1e98ad4bfb96 100644 --- a/src/http/server/request_processor.cr +++ b/src/http/server/request_processor.cr @@ -23,7 +23,6 @@ class HTTP::Server::RequestProcessor end def process(input, output) - must_close = true response = Response.new(output) begin @@ -47,7 +46,7 @@ class HTTP::Server::RequestProcessor response.headers["Connection"] = "keep-alive" if request.keep_alive? context = Context.new(request, response) - begin + Log.with_context do @handler.call(context) rescue ex : ClientError Log.debug(exception: ex.cause) { ex.message } @@ -63,13 +62,15 @@ class HTTP::Server::RequestProcessor response.output.close end - if response.upgraded? - must_close = false + output.flush + + # If there is an upgrade handler, hand over + # the connection to it and return + if upgrade_handler = response.upgrade_handler + upgrade_handler.call(output) return end - output.flush - break unless request.keep_alive? # Don't continue if the handler set `Connection` header to `close` @@ -93,12 +94,6 @@ class HTTP::Server::RequestProcessor end rescue IO::Error # IO-related error, nothing to do - ensure - begin - input.close if must_close - rescue IO::Error - # IO-related error, nothing to do - end end end end diff --git a/src/http/server/response.cr b/src/http/server/response.cr index 07ff9d24f841..a2651339fcca 100644 --- a/src/http/server/response.cr +++ b/src/http/server/response.cr @@ -14,7 +14,6 @@ class HTTP::Server # # A response can be upgraded with the `upgrade` method. Once invoked, headers # are written and the connection `IO` (a socket) is yielded to the given block. - # The block must invoke `close` afterwards, the server won't do it in this case. # This is useful to implement protocol upgrades, such as websockets. class Response < IO # The response headers (`HTTP::Headers`). These must be set before writing to the response. @@ -34,6 +33,9 @@ class HTTP::Server # body. If not set, the default value is 200 (OK). property status : HTTP::Status + # :nodoc: + property upgrade_handler : (IO ->)? + @cookies : HTTP::Cookies? # :nodoc: @@ -41,7 +43,6 @@ class HTTP::Server @headers = Headers.new @status = :ok @wrote_headers = false - @upgraded = false @output = output = @original_output = Output.new(@io) output.response = self end @@ -53,7 +54,6 @@ class HTTP::Server @cookies = nil @status = :ok @wrote_headers = false - @upgraded = false @output = @original_output @original_output.reset end @@ -96,19 +96,11 @@ class HTTP::Server raise "Can't read from HTTP::Server::Response" end - # Upgrades this response, writing headers and yieling the connection `IO` (a socket) to the given block. - # The block must invoke `close` afterwards, the server won't do it in this case. + # Upgrades this response, writing headers and yielding the connection `IO` (a socket) to the given block. # This is useful to implement protocol upgrades, such as websockets. - def upgrade - @upgraded = true + def upgrade(&block : IO ->) write_headers - flush - yield @io - end - - # :nodoc: - def upgraded? - @upgraded + @upgrade_handler = block end # Flushes the output. This method must be implemented if wrapping the response output. @@ -183,6 +175,7 @@ class HTTP::Server def initialize(@io) @chunked = false + @closed = false end def reset @@ -211,7 +204,7 @@ class HTTP::Server ensure_headers_written if @chunked - slice.size.to_s(16, @io) + slice.size.to_s(@io, 16) @io << "\r\n" @io.write(slice) @io << "\r\n" @@ -223,14 +216,14 @@ class HTTP::Server raise ClientError.new("Error while writing data to the client", ex) end - def closed? + def closed? : Bool @closed end def close return if closed? - unless response.wrote_headers? + if !response.wrote_headers? && !response.headers.has_key?("Content-Length") response.content_length = @out_count end diff --git a/src/http/web_socket.cr b/src/http/web_socket.cr index 5afe899797de..1770c42e9d8a 100644 --- a/src/http/web_socket.cr +++ b/src/http/web_socket.cr @@ -48,18 +48,23 @@ class HTTP::WebSocket new(Protocol.new(host, path, port, tls, headers)) end + # Called when the server sends a ping to a client. def on_ping(&@on_ping : String ->) end + # Called when the server receives a pong from a client. def on_pong(&@on_pong : String ->) end + # Called when the server receives a text message from a client. def on_message(&@on_message : String ->) end + # Called when the server receives a binary message from a client. def on_binary(&@on_binary : Bytes ->) end + # Called when the server closes a client's connection. def on_close(&@on_close : CloseCode, String ->) end @@ -67,6 +72,7 @@ class HTTP::WebSocket raise IO::Error.new "Closed socket" if closed? end + # Sends a message payload (message) to the client. def send(message) check_open @ws.send(message) @@ -102,12 +108,29 @@ class HTTP::WebSocket close(nil, message) end + # Sends a close frame to the client, and closes the connection. + # The close frame may contain a body (message) that indicates the reason for closing. def close(code : CloseCode | Int? = nil, message = nil) return if closed? @closed = true @ws.close(code, message) end + # Continuously receives messages and calls previously set callbacks until the websocket is closed. + # Ping and pong messages are automatically handled. + # + # ``` + # # Open websocket connection + # ws = WebSocket.new(uri) + # + # # Set callback + # ws.on_message do |msg| + # ws.send "response" + # end + # + # # Start infinite loop + # ws.run + # ``` def run loop do begin @@ -159,7 +182,7 @@ class HTTP::WebSocket message = @current_message.gets_to_end @on_close.try &.call(code, message) - close(code) + close @current_message.clear break diff --git a/src/http/web_socket/protocol.cr b/src/http/web_socket/protocol.cr index 302eb2541237..e9cfacfd5646 100644 --- a/src/http/web_socket/protocol.cr +++ b/src/http/web_socket/protocol.cr @@ -296,7 +296,7 @@ class HTTP::WebSocket::Protocol handshake.to_io(socket) socket.flush - handshake_response = HTTP::Client::Response.from_io(socket) + handshake_response = HTTP::Client::Response.from_io(socket, ignore_body: true) unless handshake_response.status.switching_protocols? raise Socket::Error.new("Handshake got denied. Status code was #{handshake_response.status.code}.") end diff --git a/src/humanize.cr b/src/humanize.cr index 794032bb5561..9f2d87640400 100644 --- a/src/humanize.cr +++ b/src/humanize.cr @@ -64,7 +64,7 @@ struct Number SI_PREFIXES = { {'y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm'}, {nil, 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'} } # SI prefixes used by `#humanize`. Equal to `SI_PREFIXES` but prepends the - # prefix with a space charater. + # prefix with a space character. SI_PREFIXES_PADDED = ->(magnitude : Int32, _number : Float64) do magnitude = Number.prefix_index(magnitude) {magnitude, (magnitude == 0 ? " " : si_prefix(magnitude))} diff --git a/src/ini.cr b/src/ini.cr index 2aaad871d6c0..4936cbecc327 100644 --- a/src/ini.cr +++ b/src/ini.cr @@ -1,4 +1,4 @@ -class INI +module INI # Exception thrown on an INI parse error. class ParseException < Exception getter line_number : Int32 @@ -21,12 +21,12 @@ class INI # # INI.parse("[foo]\na = 1") # => {"foo" => {"a" => "1"}} # ``` - def self.parse(str : String | IO) : Hash(String, Hash(String, String)) + def self.parse(string_or_io : String | IO) : Hash(String, Hash(String, String)) ini = Hash(String, Hash(String, String)).new current_section = ini[""] = Hash(String, String).new lineno = 0 - str.each_line do |line| + string_or_io.each_line do |line| lineno += 1 next if line.empty? diff --git a/src/int.cr b/src/int.cr index da9692551fc3..87e868995f89 100644 --- a/src/int.cr +++ b/src/int.cr @@ -508,7 +508,7 @@ struct Int i = self ^ self while i < self yield i - i += 1 + i &+= 1 end end @@ -562,19 +562,46 @@ struct Int self % other end - private DIGITS_DOWNCASE = "0123456789abcdefghijklmnopqrstuvwxyz" - private DIGITS_UPCASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - private DIGITS_BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + # Returns the digits of a number in a given base. + # The digits are returned as an array with the least significant digit as the first array element. + # + # ``` + # 12345.digits # => [5, 4, 3, 2, 1] + # 12345.digits(7) # => [4, 6, 6, 0, 5] + # 12345.digits(100) # => [45, 23, 1] + # + # -12345.digits(7) # => ArgumentError + # ``` + def digits(base = 10) : Array(Int32) + if base < 2 + raise ArgumentError.new("Invalid base #{base}") + end - def to_s : String - to_s(10) - end + if self < 0 + raise ArgumentError.new("Can't request digits of negative number") + end + + if self == 0 + return [0] + end + + num = self - def to_s(io : IO) : Nil - to_s(10, io) + digits_count = (Math.log(self.to_f + 1) / Math.log(base)).ceil.to_i + + ary = Array(Int32).new(digits_count) + while num != 0 + ary << num.remainder(base).to_i + num = num.tdiv(base) + end + ary end - def to_s(base : Int, upcase : Bool = false) : String + private DIGITS_DOWNCASE = "0123456789abcdefghijklmnopqrstuvwxyz" + private DIGITS_UPCASE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + private DIGITS_BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + def to_s(base : Int = 10, *, upcase : Bool = false) : String raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 @@ -590,7 +617,12 @@ struct Int end end - def to_s(base : Int, io : IO, upcase : Bool = false) : Nil + @[Deprecated("Use `#to_s(base : Int, *, upcase : Bool = false)` instead")] + def to_s(base : Int, _upcase : Bool) : String + to_s(base, upcase: _upcase) + end + + def to_s(io : IO, base : Int = 10, *, upcase : Bool = false) : Nil raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 @@ -606,6 +638,11 @@ struct Int end end + @[Deprecated("Use `#to_s(io : IO, base : Int, *, upcase : Bool = false)` instead")] + def to_s(base : Int, io : IO, upcase : Bool = false) : Nil + to_s(io, base, upcase: upcase) + end + private def internal_to_s(base, upcase = false) # Given sizeof(self) <= 128 bits, we need at most 128 bytes for a base 2 # representation, plus one byte for the trailing 0. @@ -670,7 +707,7 @@ struct Int def next if @index < @n value = @index - @index += 1 + @index &+= 1 value else stop diff --git a/src/io.cr b/src/io.cr index 710282f8d6ca..d17f9c670651 100644 --- a/src/io.cr +++ b/src/io.cr @@ -32,7 +32,6 @@ require "c/errno" # def write(slice : Bytes) : Nil # slice.size.times { |i| @slice[i] = slice[i] } # @slice += slice.size -# nil # end # end # @@ -260,7 +259,7 @@ abstract class IO end # Writes a formatted string to this IO. - # For details on the format string, see `Kernel::sprintf`. + # For details on the format string, see top-level `::printf`. def printf(format_string, *args) : Nil printf format_string, args end @@ -268,7 +267,6 @@ abstract class IO # :ditto: def printf(format_string, args : Array | Tuple) : Nil String::Formatter(typeof(args)).new(format_string, args, self).format - nil end # Reads a single byte from this `IO`. Returns `nil` if there is no more @@ -471,6 +469,7 @@ abstract class IO else write(slice) end + nil end @@ -1114,12 +1113,12 @@ abstract class IO # # io2.to_s # => "hello" # ``` - def self.copy(src, dst) : UInt64 + def self.copy(src, dst) : Int64 buffer = uninitialized UInt8[4096] - count = 0_u64 + count = 0_i64 while (len = src.read(buffer.to_slice).to_i32) > 0 dst.write buffer.to_slice[0, len] - count += len + count &+= len end count end @@ -1134,20 +1133,43 @@ abstract class IO # # io2.to_s # => "hel" # ``` - def self.copy(src, dst, limit : Int) : UInt64 + def self.copy(src, dst, limit : Int) : Int64 raise ArgumentError.new("Negative limit") if limit < 0 - limit = limit.to_u64 + limit = limit.to_i64 buffer = uninitialized UInt8[4096] remaining = limit while (len = src.read(buffer.to_slice[0, Math.min(buffer.size, Math.max(remaining, 0))])) > 0 dst.write buffer.to_slice[0, len] - remaining -= len + remaining &-= len end limit - remaining end + # Compares two streams *stream1* to *stream2* to determine if they are identical. + # Returns `true` if content are the same, `false` otherwise. + # + # ``` + # File.write("afile", "123") + # stream1 = File.open("afile") + # stream2 = IO::Memory.new("123") + # IO.same_content?(stream1, stream2) # => true + # ``` + def self.same_content?(stream1 : IO, stream2 : IO) + buf1 = uninitialized UInt8[1024] + buf2 = uninitialized UInt8[1024] + + while true + read1 = stream1.read(buf1.to_slice) + read2 = stream2.read_fully?(buf2.to_slice[0, read1]) + return false unless read2 + + return false if buf1.to_unsafe.memcmp(buf2.to_unsafe, read1) != 0 + return true if read1 == 0 + end + end + private struct LineIterator(I, A, N) include Iterator(String) diff --git a/src/io/buffered.cr b/src/io/buffered.cr index 1ed9b132d8f5..be8275db3f9a 100644 --- a/src/io/buffered.cr +++ b/src/io/buffered.cr @@ -158,7 +158,6 @@ module IO::Buffered slice.copy_to(out_buffer + @out_count, count) @out_count += count - nil end # :nodoc: diff --git a/src/io/encoding.cr b/src/io/encoding.cr index 174029fbb31c..07c7661afd73 100644 --- a/src/io/encoding.cr +++ b/src/io/encoding.cr @@ -80,14 +80,6 @@ class IO @in_buffer_left = LibC::SizeT.new(io.read(@buffer)) end - # If we just have a few bytes to decode, read more, just in case these don't produce a character - if @in_buffer_left < 16 - buffer_remaining = BUFFER_SIZE - @in_buffer_left - (@in_buffer - @buffer.to_unsafe) - @buffer.copy_from(@in_buffer, @in_buffer_left) - @in_buffer = @buffer.to_unsafe - @in_buffer_left += LibC::SizeT.new(io.read(Slice.new(@in_buffer + @in_buffer_left, buffer_remaining))) - end - # If, after refilling the buffer, we couldn't read new bytes # it means we reached the end break if @in_buffer_left == 0 diff --git a/src/io/evented.cr b/src/io/evented.cr index 15e802eb1c07..2b47725b3837 100644 --- a/src/io/evented.cr +++ b/src/io/evented.cr @@ -117,11 +117,11 @@ module IO::Evented # :nodoc: def wait_readable(timeout = @read_timeout) - wait_readable(timeout: timeout) { |err| raise err } + wait_readable(timeout: timeout) { raise TimeoutError.new("Read timed out") } end # :nodoc: - def wait_readable(timeout = @read_timeout) : Nil + def wait_readable(timeout = @read_timeout, *, raise_if_closed = true) : Nil readers = @readers.get { Deque(Fiber).new } readers << Fiber.current add_read_event(timeout) @@ -129,10 +129,10 @@ module IO::Evented if @read_timed_out @read_timed_out = false - yield TimeoutError.new("Read timed out") + yield end - check_open + check_open if raise_if_closed end private def add_read_event(timeout = @read_timeout) : Nil @@ -142,7 +142,7 @@ module IO::Evented # :nodoc: def wait_writable(timeout = @write_timeout) - wait_writable(timeout: timeout) { |err| raise err } + wait_writable(timeout: timeout) { raise TimeoutError.new("Write timed out") } end # :nodoc: @@ -154,7 +154,7 @@ module IO::Evented if @write_timed_out @write_timed_out = false - yield TimeoutError.new("Write timed out") + yield end check_open diff --git a/src/io/file_descriptor.cr b/src/io/file_descriptor.cr index 64e30c155811..35754c9ec97b 100644 --- a/src/io/file_descriptor.cr +++ b/src/io/file_descriptor.cr @@ -32,27 +32,7 @@ class IO::FileDescriptor < IO # :nodoc: def self.from_stdio(fd) - {% if flag?(:win32) %} - new(fd) - {% else %} - # If we have a TTY for stdin/out/err, it is possibly a shared terminal. - # We need to reopen it to use O_NONBLOCK without causing other programs to break - - # Figure out the terminal TTY name. If ttyname fails we have a non-tty, or something strange. - # For non-tty we set flush_on_newline to true for reasons described in STDOUT and STDERR docs. - path = uninitialized UInt8[256] - ret = LibC.ttyname_r(fd, path, 256) - return new(fd).tap(&.flush_on_newline=(true)) unless ret == 0 - - clone_fd = LibC.open(path, LibC::O_RDWR) - return new(fd).tap(&.flush_on_newline=(true)) if clone_fd == -1 - - # We don't buffer output for TTY devices to see their output right away - io = new(clone_fd) - io.close_on_exec = true - io.sync = true - io - {% end %} + Crystal::System::FileDescriptor.from_stdio(fd) end def blocking diff --git a/src/io/memory.cr b/src/io/memory.cr index 4a2f0d51852d..e447c9f5da93 100644 --- a/src/io/memory.cr +++ b/src/io/memory.cr @@ -104,8 +104,6 @@ class IO::Memory < IO @pos += count @bytesize = @pos if @pos > @bytesize - - nil end # See `IO#write_byte`. Raises if this `IO::Memory` is non-writeable, @@ -206,7 +204,7 @@ class IO::Memory < IO end # :nodoc: - def skip_to_end + def skip_to_end : Nil check_open @pos = @bytesize diff --git a/src/io/stapled.cr b/src/io/stapled.cr index 24e9067d33e6..3fc6e2f92d7d 100644 --- a/src/io/stapled.cr +++ b/src/io/stapled.cr @@ -57,8 +57,15 @@ class IO::Stapled < IO @reader.skip(bytes_count) end + # Skips `reader`. + def skip_to_end : Nil + check_open + + @reader.skip_to_end + end + # Writes a byte to `writer`. - def write_byte(byte : UInt8) : Nil + def write_byte(byte : UInt8) check_open @writer.write_byte(byte) diff --git a/src/iterator.cr b/src/iterator.cr index da5906bc6520..5da4299a060b 100644 --- a/src/iterator.cr +++ b/src/iterator.cr @@ -338,18 +338,17 @@ module Iterator(T) def initialize(@iterator : I) end - def next + def next : {T, T} | Iterator::Stop elem = wrapped_next - return elem if elem.is_a?(Iterator::Stop) + last_elem = @last_elem - if @last_elem.is_a?(Iterator::Stop) + if last_elem.is_a?(Iterator::Stop) @last_elem = elem - self.next else + value = {last_elem, elem} @last_elem, elem = elem, @last_elem - - {elem, @last_elem} + value end end end diff --git a/src/json.cr b/src/json.cr index b963373b190d..713eafb03ea1 100644 --- a/src/json.cr +++ b/src/json.cr @@ -28,7 +28,7 @@ # Most types in the standard library implement these methods. For user-defined types # you can define a `self.new(pull : JSON::PullParser)` for parsing and # `to_json(builder : JSON::Builder)` for serializing. The following sections -# show convenient ways to do this using either `JSON.mapping` or `JSON::Serializable`. +# show convenient ways to do this using `JSON::Serializable`. # # NOTE: JSON object keys are always strings but they can still be parsed # and deserialized to other types. To deserialize, define a @@ -46,12 +46,6 @@ # {1.5 => 2}.to_json # => "{\"1.5\":2}" # ``` # -# ### Parsing and generating with `JSON.mapping` -# -# Use `JSON.mapping` to define how an object is mapped to JSON, making it -# the recommended easy, type-safe and efficient option for parsing and generating -# JSON. Refer to that module's documentation to learn about it. -# # ### Parsing with `JSON.parse` # # `JSON.parse` will return an `Any`, which is a convenient wrapper around all possible JSON types, @@ -83,7 +77,7 @@ # end # ``` # -# Parsing with `JSON.parse` is useful for dealing with a dynamic JSON structure but is slower than using `JSON.mapping`. +# Parsing with `JSON.parse` is useful for dealing with a dynamic JSON structure. # # ### Generating with `JSON.build` # @@ -112,7 +106,7 @@ # # `to_json`, `to_json(IO)` and `to_json(JSON::Builder)` methods are provided # for primitive types, but you need to define `to_json(JSON::Builder)` -# for custom objects, either manually or using `JSON.mapping`. +# for custom objects, either manually or using `JSON::Serializable`. module JSON # Generic JSON error. class Error < Exception diff --git a/src/json/any.cr b/src/json/any.cr index b68e18543f41..55f4533e6fbb 100644 --- a/src/json/any.cr +++ b/src/json/any.cr @@ -119,33 +119,28 @@ struct JSON::Any # Traverses the depth of a structure and returns the value. # Returns `nil` if not found. - def dig?(key : String | Int, *subkeys) - if value = self[key]? - value.dig?(*subkeys) - end + def dig?(index_or_key : String | Int, *subkeys) : JSON::Any? + self[index_or_key]?.try &.dig?(*subkeys) end # :nodoc: - def dig?(key : String | Int) + def dig?(index_or_key : String | Int) : JSON::Any? case @raw when Hash, Array - self[key]? + self[index_or_key]? else nil end end # Traverses the depth of a structure and returns the value, otherwise raises. - def dig(key : String | Int, *subkeys) - if (value = self[key]) && value.responds_to?(:dig) - return value.dig(*subkeys) - end - raise "JSON::Any value not diggable for key: #{key.inspect}" + def dig(index_or_key : String | Int, *subkeys) : JSON::Any + self[index_or_key].dig(*subkeys) end # :nodoc: - def dig(key : String | Int) - self[key] + def dig(index_or_key : String | Int) : JSON::Any + self[index_or_key] end # Checks that the underlying value is `Nil`, and returns `nil`. diff --git a/src/json/builder.cr b/src/json/builder.cr index 8f6dc9f79a1d..0d8d2017d1af 100644 --- a/src/json/builder.cr +++ b/src/json/builder.cr @@ -131,7 +131,7 @@ class JSON::Builder io << '0' if ord < 0x1000 io << '0' if ord < 0x100 io << '0' if ord < 0x10 - ord.to_s(16, io) + ord.to_s(io, 16) reader.next_char start_pos = reader.pos next diff --git a/src/json/from_json.cr b/src/json/from_json.cr index e0b1c66a62c3..a9903648c785 100644 --- a/src/json/from_json.cr +++ b/src/json/from_json.cr @@ -310,7 +310,7 @@ def Union.new(pull : JSON::PullParser) {% end %} end -# Reads a string from JSON parser as a time formated according to [RFC 3339](https://tools.ietf.org/html/rfc3339) +# Reads a string from JSON parser as a time formatted according to [RFC 3339](https://tools.ietf.org/html/rfc3339) # or other variations of [ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf). # # The JSON format itself does not specify a time data type, this method just diff --git a/src/json/mapping.cr b/src/json/mapping.cr deleted file mode 100644 index b486050cd581..000000000000 --- a/src/json/mapping.cr +++ /dev/null @@ -1,253 +0,0 @@ -module JSON - # The `JSON.mapping` macro defines how an object is mapped to JSON. - # - # ### Example - # - # ``` - # require "json" - # - # class Location - # JSON.mapping( - # lat: Float64, - # lng: Float64, - # ) - # end - # - # class House - # JSON.mapping( - # address: String, - # location: {type: Location, nilable: true}, - # ) - # end - # - # house = House.from_json(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}})) - # house.address # => "Crystal Road 1234" - # house.location # => # - # house.to_json # => %({"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}}) - # - # houses = Array(House).from_json(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}])) - # houses.size # => 1 - # houses.to_json # => %([{"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}}]) - # ``` - # - # ### Usage - # - # `JSON.mapping` must receive a series of named arguments, or a named tuple literal, or a hash literal, - # whose keys will define Crystal properties. - # - # The value of each key can be a type. Primitive types (numbers, string, boolean and nil) - # are supported, as well as custom objects which use `JSON.mapping` or define a `new` method - # that accepts a `JSON::PullParser` and returns an object from it. Union types are supported, - # if multiple types in the union can be mapped from the JSON, it is undefined which one will be chosen. - # - # The value can also be another hash literal with the following options: - # * **type**: (required) the type described above (you can use `JSON::Any` too) - # * **key**: the property name in the JSON document (as opposed to the property name in the Crystal code) - # * **nilable**: if `true`, the property can be `Nil`. Passing `T?` as a type has the same effect. - # * **default**: value to use if the property is missing in the JSON document, or if it's `null` and `nilable` was not set to `true`. If the default value creates a new instance of an object (for example `[1, 2, 3]` or `SomeObject.new`), a different instance will be used each time a JSON document is parsed. - # * **emit_null**: if `true`, emits a `null` value for nilable properties (by default nulls are not emitted) - # * **converter**: specify an alternate type for parsing and generation. The converter must define `from_json(JSON::PullParser)` and `to_json(value, JSON::Builder)` as class methods. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`. - # * **root**: assume the value is inside a JSON object with a given key (see `Object.from_json(string_or_io, root)`) - # * **setter**: if `true`, will generate a setter for the variable, `true` by default - # * **getter**: if `true`, will generate a getter for the variable, `true` by default - # * **presence**: if `true`, a `{{key}}_present?` method will be generated when the key was present (even if it has a `null` value), `false` by default - # - # This macro by default defines getters and setters for each variable (this can be overrided with *setter* and *getter*). - # The mapping doesn't define a constructor accepting these variables as arguments, but you can provide an overload. - # - # The macro basically defines a constructor accepting a `JSON::PullParser` that reads from - # it and initializes this type's instance variables. It also defines a `to_json(JSON::Builder)` method - # by invoking `to_json(JSON::Builder)` on each of the properties (unless a converter is specified, in - # which case `to_json(value, JSON::Builder)` is invoked). - # - # This macro also declares instance variables of the types given in the mapping. - # - # If *strict* is `true`, unknown properties in the JSON - # document will raise a parse exception. The default is `false`, so unknown properties - # are silently ignored. - macro mapping(_properties_, strict = false) - {% for key, value in _properties_ %} - {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} - {% end %} - - {% for key, value in _properties_ %} - {% _properties_[key][:key_id] = key.id.gsub(/\?$/, "") %} - {% end %} - - {% for key, value in _properties_ %} - @{{value[:key_id]}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }} - - {% if value[:setter] == nil ? true : value[:setter] %} - def {{value[:key_id]}}=(_{{value[:key_id]}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }}) - @{{value[:key_id]}} = _{{value[:key_id]}} - end - {% end %} - - {% if value[:getter] == nil ? true : value[:getter] %} - def {{key.id}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }} - @{{value[:key_id]}} - end - {% end %} - - {% if value[:presence] %} - @{{value[:key_id]}}_present : Bool = false - - def {{value[:key_id]}}_present? - @{{value[:key_id]}}_present - end - {% end %} - {% end %} - - def initialize(%pull : ::JSON::PullParser) - {% for key, value in _properties_ %} - %var{key.id} = nil - %found{key.id} = false - {% end %} - - %location = %pull.location - begin - %pull.read_begin_object - rescue exc : ::JSON::ParseException - raise ::JSON::MappingError.new(exc.message, self.class.to_s, nil, *%location, exc) - end - until %pull.kind.end_object? - %key_location = %pull.location - key = %pull.read_object_key - case key - {% for key, value in _properties_ %} - when {{value[:key] || value[:key_id].stringify}} - %found{key.id} = true - begin - %var{key.id} = - {% if value[:nilable] || value[:default] != nil %} %pull.read_null_or { {% end %} - - {% if value[:root] %} - %pull.on_key!({{value[:root]}}) do - {% end %} - - {% if value[:converter] %} - {{value[:converter]}}.from_json(%pull) - {% elsif value[:type].is_a?(Path) || value[:type].is_a?(Generic) %} - {{value[:type]}}.new(%pull) - {% else %} - ::Union({{value[:type]}}).new(%pull) - {% end %} - - {% if value[:root] %} - end - {% end %} - - {% if value[:nilable] || value[:default] != nil %} } {% end %} - rescue exc : ::JSON::ParseException - raise ::JSON::MappingError.new(exc.message, self.class.to_s, {{value[:key] || value[:key_id].stringify}}, *%key_location, exc) - end - {% end %} - else - {% if strict %} - raise ::JSON::MappingError.new("Unknown JSON attribute: #{key}", self.class.to_s, nil, *%key_location, nil) - {% else %} - %pull.skip - {% end %} - end - end - %pull.read_next - - {% for key, value in _properties_ %} - {% unless value[:nilable] || value[:default] != nil %} - if %var{key.id}.nil? && !%found{key.id} && !::Union({{value[:type]}}).nilable? - raise ::JSON::MappingError.new("Missing JSON attribute: {{(value[:key] || value[:key_id]).id}}", self.class.to_s, nil, *%location, nil) - end - {% end %} - - {% if value[:nilable] %} - {% if value[:default] != nil %} - @{{value[:key_id]}} = %found{key.id} ? %var{key.id} : {{value[:default]}} - {% else %} - @{{value[:key_id]}} = %var{key.id} - {% end %} - {% elsif value[:default] != nil %} - @{{value[:key_id]}} = %var{key.id}.nil? ? {{value[:default]}} : %var{key.id} - {% else %} - @{{value[:key_id]}} = (%var{key.id}).as({{value[:type]}}) - {% end %} - - {% if value[:presence] %} - @{{value[:key_id]}}_present = %found{key.id} - {% end %} - {% end %} - end - - def to_json(json : ::JSON::Builder) - json.object do - {% for key, value in _properties_ %} - _{{value[:key_id]}} = @{{value[:key_id]}} - - {% unless value[:emit_null] %} - unless _{{value[:key_id]}}.nil? - {% end %} - - json.field({{value[:key] || value[:key_id].stringify}}) do - {% if value[:root] %} - {% if value[:emit_null] %} - if _{{value[:key_id]}}.nil? - nil.to_json(json) - else - {% end %} - - json.object do - json.field({{value[:root]}}) do - {% end %} - - {% if value[:converter] %} - if _{{value[:key_id]}} - {{ value[:converter] }}.to_json(_{{value[:key_id]}}, json) - else - nil.to_json(json) - end - {% else %} - _{{value[:key_id]}}.to_json(json) - {% end %} - - {% if value[:root] %} - {% if value[:emit_null] %} - end - {% end %} - end - end - {% end %} - end - - {% unless value[:emit_null] %} - end - {% end %} - {% end %} - end - end - end - - # This is a convenience method to allow invoking `JSON.mapping` - # with named arguments instead of with a hash/named-tuple literal. - macro mapping(**_properties_) - ::JSON.mapping({{_properties_}}) - end - - class MappingError < ParseException - getter klass : String - getter attribute : String? - - def initialize(message : String?, @klass : String, @attribute : String?, line_number : Int32, column_number : Int32, cause) - message = String.build do |io| - io << message - io << "\n parsing " - io << klass - if attribute = @attribute - io << '#' << attribute - end - end - super(message, line_number, column_number, cause) - if cause - @line_number, @column_number = cause.location - end - end - end -end diff --git a/src/json/serialization.cr b/src/json/serialization.cr index 3793f60f099d..b2dd18c7ba17 100644 --- a/src/json/serialization.cr +++ b/src/json/serialization.cr @@ -183,7 +183,7 @@ module JSON begin pull.read_begin_object rescue exc : ::JSON::ParseException - raise ::JSON::MappingError.new(exc.message, self.class.to_s, nil, *%location, exc) + raise ::JSON::SerializableError.new(exc.message, self.class.to_s, nil, *%location, exc) end until pull.kind.end_object? %key_location = pull.location @@ -212,7 +212,7 @@ module JSON {% if value[:nilable] || value[:has_default] %} } {% end %} rescue exc : ::JSON::ParseException - raise ::JSON::MappingError.new(exc.message, self.class.to_s, {{value[:key]}}, *%key_location, exc) + raise ::JSON::SerializableError.new(exc.message, self.class.to_s, {{value[:key]}}, *%key_location, exc) end {% end %} else @@ -224,7 +224,7 @@ module JSON {% for name, value in properties %} {% unless value[:nilable] || value[:has_default] %} if %var{name}.nil? && !%found{name} && !::Union({{value[:type]}}).nilable? - raise ::JSON::MappingError.new("Missing JSON attribute: {{value[:key].id}}", self.class.to_s, nil, *%location, nil) + raise ::JSON::SerializableError.new("Missing JSON attribute: {{value[:key].id}}", self.class.to_s, nil, *%location, nil) end {% end %} @@ -329,7 +329,7 @@ module JSON module Strict protected def on_unknown_json_attribute(pull, key, key_location) - raise ::JSON::MappingError.new("Unknown JSON attribute: #{key}", self.class.to_s, nil, *key_location, nil) + raise ::JSON::SerializableError.new("Unknown JSON attribute: #{key}", self.class.to_s, nil, *key_location, nil) end end @@ -341,7 +341,7 @@ module JSON json_unmapped[key] = begin JSON::Any.new(pull) rescue exc : ::JSON::ParseException - raise ::JSON::MappingError.new(exc.message, self.class.to_s, key, *key_location, exc) + raise ::JSON::SerializableError.new(exc.message, self.class.to_s, key, *key_location, exc) end end @@ -414,7 +414,7 @@ module JSON end unless discriminator_value - raise ::JSON::MappingError.new("Missing JSON discriminator field '{{field.id}}'", to_s, nil, *location, nil) + raise ::JSON::SerializableError.new("Missing JSON discriminator field '{{field.id}}'", to_s, nil, *location, nil) end case discriminator_value @@ -423,9 +423,29 @@ module JSON {{value.id}}.from_json(json) {% end %} else - raise ::JSON::MappingError.new("Unknown '{{field.id}}' discriminator value: #{discriminator_value.inspect}", to_s, nil, *location, nil) + raise ::JSON::SerializableError.new("Unknown '{{field.id}}' discriminator value: #{discriminator_value.inspect}", to_s, nil, *location, nil) end end end end + + class SerializableError < ParseException + getter klass : String + getter attribute : String? + + def initialize(message : String?, @klass : String, @attribute : String?, line_number : Int32, column_number : Int32, cause) + message = String.build do |io| + io << message + io << "\n parsing " + io << klass + if attribute = @attribute + io << '#' << attribute + end + end + super(message, line_number, column_number, cause) + if cause + @line_number, @column_number = cause.location + end + end + end end diff --git a/src/json/to_json.cr b/src/json/to_json.cr index 75e9334cd2a0..6f0228eb0382 100644 --- a/src/json/to_json.cr +++ b/src/json/to_json.cr @@ -166,7 +166,7 @@ struct Enum end struct Time - # Emits a string formated according to [RFC 3339](https://tools.ietf.org/html/rfc3339) + # Emits a string formatted according to [RFC 3339](https://tools.ietf.org/html/rfc3339) # ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # # The JSON format itself does not specify a time data type, this method just @@ -179,16 +179,17 @@ struct Time end end -# Converter to be used with `JSON.mapping` +# Converter to be used with `JSON::Serializable` # to serialize the `Array(T)` elements with the custom converter. # # ``` # require "json" # # class TimestampArray -# JSON.mapping({ -# dates: {type: Array(Time), converter: JSON::ArrayConverter(Time::EpochConverter)}, -# }) +# include JSON::Serializable +# +# @[JSON::Field(converter: JSON::ArrayConverter(Time::EpochConverter))] +# property dates : Array(Time) # end # # timestamp = TimestampArray.from_json(%({"dates":[1459859781,1567628762]})) @@ -205,16 +206,17 @@ module JSON::ArrayConverter(Converter) end end -# Converter to be used with `JSON.mapping` +# Converter to be used with `JSON::Serializable` # to serialize the `Hash(K, V)` values elements with the custom converter. # # ``` # require "json" # # class TimestampHash -# JSON.mapping({ -# birthdays: {type: Hash(String, Time), converter: JSON::HashValueConverter(Time::EpochConverter)}, -# }) +# include JSON::Serializable +# +# @[JSON::Field(converter: JSON::HashValueConverter(Time::EpochConverter))] +# birthdays : Hash(String, Time) # end # # timestamp = TimestampHash.from_json(%({"birthdays":{"foo":1459859781,"bar":1567628762}})) @@ -233,7 +235,7 @@ module JSON::HashValueConverter(Converter) end end -# Converter to be used with `JSON.mapping` and `YAML.mapping` +# Converter to be used with `JSON::Serializable` and `YAML::Serializable` # to serialize a `Time` instance as the number of seconds # since the unix epoch. See `Time#to_unix`. # @@ -241,9 +243,10 @@ end # require "json" # # class Person -# JSON.mapping({ -# birth_date: {type: Time, converter: Time::EpochConverter}, -# }) +# include JSON::Serializable +# +# @[JSON::Field(converter: Time::EpochConverter)] +# birth_date : Time # end # # person = Person.from_json(%({"birth_date": 1459859781})) @@ -256,7 +259,7 @@ module Time::EpochConverter end end -# Converter to be used with `JSON.mapping` and `YAML.mapping` +# Converter to be used with `JSON::Serializable` and `YAML::Serializable` # to serialize a `Time` instance as the number of milliseconds # since the unix epoch. See `Time#to_unix_ms`. # @@ -264,9 +267,10 @@ end # require "json" # # class Timestamp -# JSON.mapping({ -# value: {type: Time, converter: Time::EpochMillisConverter}, -# }) +# include JSON::Serializable +# +# @[JSON::Field(converter: Time::EpochMillisConverter)] +# value : Time # end # # timestamp = Timestamp.from_json(%({"value": 1459860483856})) @@ -279,7 +283,7 @@ module Time::EpochMillisConverter end end -# Converter to be used with `JSON.mapping` to read the raw +# Converter to be used with `JSON::Serializable` to read the raw # value of a JSON object property as a `String`. # # It can be useful to read ints and floats without losing precision, @@ -290,9 +294,10 @@ end # require "json" # # class Raw -# JSON.mapping({ -# value: {type: String, converter: String::RawConverter}, -# }) +# include JSON::Serializable +# +# @[JSON::Field(converter: String::RawConverter)] +# value : String # end # # raw = Raw.from_json(%({"value": 123456789876543212345678987654321})) diff --git a/src/kernel.cr b/src/kernel.cr index 74410b97dc6c..b0d5cfc38eca 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -467,6 +467,10 @@ end # passed as the second argument to the block, if the program terminates # normally or `exit(status)` is called explicitly, then the second argument # will be `nil`. +# +# NOTE: If `at_exit` is called inside an `at_exit` handler, it will be called +# right after the current `at_exit` handler ends, and then other handlers +# will be invoked. def at_exit(&handler : Int32, Exception? ->) : Nil Crystal::AtExitHandlers.add(handler) end diff --git a/src/lib_c/aarch64-linux-gnu/c/elf.cr b/src/lib_c/aarch64-linux-gnu/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/aarch64-linux-gnu/c/link.cr b/src/lib_c/aarch64-linux-gnu/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/aarch64-linux-gnu/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/aarch64-linux-gnu/c/stdarg.cr b/src/lib_c/aarch64-linux-gnu/c/stdarg.cr index 882d4f51d35c..965355556d71 100644 --- a/src/lib_c/aarch64-linux-gnu/c/stdarg.cr +++ b/src/lib_c/aarch64-linux-gnu/c/stdarg.cr @@ -1,3 +1,10 @@ lib LibC - type VaList = Void* + # based on https://github.com/llvm/llvm-project/blob/bf1cdc2c6c0460b7121ac653c796ef4995b1dfa9/clang/lib/AST/ASTContext.cpp#L7678-L7739 + struct VaList + __stack : Void* + __gr_top : Void* + __vr_top : Void* + __gr_offs : Int32 + __vr_offs : Int32 + end end diff --git a/src/lib_c/aarch64-linux-musl/c/elf.cr b/src/lib_c/aarch64-linux-musl/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/aarch64-linux-musl/c/link.cr b/src/lib_c/aarch64-linux-musl/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/aarch64-linux-musl/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/aarch64-linux-musl/c/stdarg.cr b/src/lib_c/aarch64-linux-musl/c/stdarg.cr index fcad7714f16a..965355556d71 100644 --- a/src/lib_c/aarch64-linux-musl/c/stdarg.cr +++ b/src/lib_c/aarch64-linux-musl/c/stdarg.cr @@ -1,10 +1,10 @@ lib LibC - struct VaListTag - gp_offset : UInt - fp_offset : UInt - overflow_arg_area : Void* - reg_save_area : Void* + # based on https://github.com/llvm/llvm-project/blob/bf1cdc2c6c0460b7121ac653c796ef4995b1dfa9/clang/lib/AST/ASTContext.cpp#L7678-L7739 + struct VaList + __stack : Void* + __gr_top : Void* + __vr_top : Void* + __gr_offs : Int32 + __vr_offs : Int32 end - - type VaList = VaListTag[1] end diff --git a/src/lib_c/arm-linux-gnueabihf/c/elf.cr b/src/lib_c/arm-linux-gnueabihf/c/elf.cr new file mode 100644 index 000000000000..32a0dc3152d1 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt32T + alias Elf_Off = UInt32T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Word # Segment size in file + memsz : Elf_Word # Segment size in memory + flags : Elf_Word # Segment flags + align : Elf_Word # Segment alignment + end +end diff --git a/src/lib_c/arm-linux-gnueabihf/c/link.cr b/src/lib_c/arm-linux-gnueabihf/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/arm-linux-gnueabihf/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/i386-linux-gnu/c/dirent.cr b/src/lib_c/i386-linux-gnu/c/dirent.cr index 3cb61b21129e..e4674e3bf0f4 100644 --- a/src/lib_c/i386-linux-gnu/c/dirent.cr +++ b/src/lib_c/i386-linux-gnu/c/dirent.cr @@ -7,7 +7,7 @@ lib LibC struct Dirent d_ino : InoT - d_off : Long + d_off : OffT d_reclen : UShort d_type : Char d_name : StaticArray(Char, 256) @@ -15,6 +15,6 @@ lib LibC fun closedir(dirp : DIR*) : Int fun opendir(name : Char*) : DIR* - fun readdir(dirp : DIR*) : Dirent* + fun readdir = readdir64(dirp : DIR*) : Dirent* fun rewinddir(dirp : DIR*) : Void end diff --git a/src/lib_c/i386-linux-gnu/c/elf.cr b/src/lib_c/i386-linux-gnu/c/elf.cr new file mode 100644 index 000000000000..32a0dc3152d1 --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt32T + alias Elf_Off = UInt32T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Word # Segment size in file + memsz : Elf_Word # Segment size in memory + flags : Elf_Word # Segment flags + align : Elf_Word # Segment alignment + end +end diff --git a/src/lib_c/i386-linux-gnu/c/fcntl.cr b/src/lib_c/i386-linux-gnu/c/fcntl.cr index 1a8fd2f1787b..2a12d9b4857f 100644 --- a/src/lib_c/i386-linux-gnu/c/fcntl.cr +++ b/src/lib_c/i386-linux-gnu/c/fcntl.cr @@ -28,5 +28,5 @@ lib LibC end fun fcntl(fd : Int, cmd : Int, ...) : Int - fun open(file : Char*, oflag : Int, ...) : Int + fun open = open64(file : Char*, oflag : Int, ...) : Int end diff --git a/src/lib_c/i386-linux-gnu/c/link.cr b/src/lib_c/i386-linux-gnu/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/i386-linux-gnu/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/i386-linux-gnu/c/stdlib.cr b/src/lib_c/i386-linux-gnu/c/stdlib.cr index 66abbaf4743d..46ffee257b84 100644 --- a/src/lib_c/i386-linux-gnu/c/stdlib.cr +++ b/src/lib_c/i386-linux-gnu/c/stdlib.cr @@ -13,8 +13,8 @@ lib LibC fun free(ptr : Void*) : Void fun getenv(name : Char*) : Char* fun malloc(size : SizeT) : Void* - fun mkstemp(template : Char*) : Int - fun mkstemps(template : Char*, suffixlen : Int) : Int + fun mkstemp = mkstemp64(template : Char*) : Int + fun mkstemps = mkstemps64(template : Char*, suffixlen : Int) : Int fun putenv(string : Char*) : Int fun realloc(ptr : Void*, size : SizeT) : Void* fun realpath(name : Char*, resolved : Char*) : Char* diff --git a/src/lib_c/i386-linux-gnu/c/sys/mman.cr b/src/lib_c/i386-linux-gnu/c/sys/mman.cr index 8c44b210a24e..158228f6946d 100644 --- a/src/lib_c/i386-linux-gnu/c/sys/mman.cr +++ b/src/lib_c/i386-linux-gnu/c/sys/mman.cr @@ -24,7 +24,7 @@ lib LibC MADV_HUGEPAGE = 14 MADV_NOHUGEPAGE = 15 - fun mmap(addr : Void*, len : SizeT, prot : Int, flags : Int, fd : Int, offset : OffT) : Void* + fun mmap = mmap64(addr : Void*, len : SizeT, prot : Int, flags : Int, fd : Int, offset : OffT) : Void* fun mprotect(addr : Void*, len : SizeT, prot : Int) : Int fun munmap(addr : Void*, len : SizeT) : Int fun madvise(addr : Void*, len : SizeT, advice : Int) : Int diff --git a/src/lib_c/i386-linux-gnu/c/sys/stat.cr b/src/lib_c/i386-linux-gnu/c/sys/stat.cr index cf2edc189e71..2ad9f557dfae 100644 --- a/src/lib_c/i386-linux-gnu/c/sys/stat.cr +++ b/src/lib_c/i386-linux-gnu/c/sys/stat.cr @@ -29,7 +29,7 @@ lib LibC struct Stat st_dev : DevT __pad1 : UShort - st_ino : InoT + __st_ino : ULong st_mode : ModeT st_nlink : NlinkT st_uid : UidT @@ -42,16 +42,15 @@ lib LibC st_atim : Timespec st_mtim : Timespec st_ctim : Timespec - __unused4 : ULong - __unused5 : ULong + st_ino : InoT end fun chmod(file : Char*, mode : ModeT) : Int - fun fstat(fd : Int, buf : Stat*) : Int - fun lstat(file : Char*, buf : Stat*) : Int + fun fstat = fstat64(fd : Int, buf : Stat*) : Int + fun lstat = lstat64(file : Char*, buf : Stat*) : Int fun mkdir(path : Char*, mode : ModeT) : Int fun mkfifo(path : Char*, mode : ModeT) : Int fun mknod(path : Char*, mode : ModeT, dev : DevT) : Int - fun stat(file : Char*, buf : Stat*) : Int + fun stat = stat64(file : Char*, buf : Stat*) : Int fun umask(mask : ModeT) : ModeT end diff --git a/src/lib_c/i386-linux-gnu/c/sys/types.cr b/src/lib_c/i386-linux-gnu/c/sys/types.cr index 7a1618b91cf2..f89d29723e65 100644 --- a/src/lib_c/i386-linux-gnu/c/sys/types.cr +++ b/src/lib_c/i386-linux-gnu/c/sys/types.cr @@ -2,17 +2,17 @@ require "../stddef" require "../stdint" lib LibC - alias BlkcntT = Long + alias BlkcntT = LongLong alias BlksizeT = Long alias ClockT = Long alias ClockidT = Int alias DevT = ULongLong alias GidT = UInt alias IdT = UInt - alias InoT = ULong + alias InoT = ULongLong alias ModeT = UInt alias NlinkT = UInt - alias OffT = Long + alias OffT = LongLong alias PidT = Int union PthreadAttrT diff --git a/src/lib_c/i386-linux-gnu/c/unistd.cr b/src/lib_c/i386-linux-gnu/c/unistd.cr index c83a01f81ffa..3cd2f6d71cdc 100644 --- a/src/lib_c/i386-linux-gnu/c/unistd.cr +++ b/src/lib_c/i386-linux-gnu/c/unistd.cr @@ -21,7 +21,7 @@ lib LibC @[ReturnsTwice] fun fork : PidT fun fsync(fd : Int) : Int - fun ftruncate(fd : Int, length : OffT) : Int + fun ftruncate = ftruncate64(fd : Int, length : OffT) : Int fun getcwd(buf : Char*, size : SizeT) : Char* fun gethostname(name : Char*, len : SizeT) : Int fun getpgid(pid : PidT) : PidT @@ -31,11 +31,11 @@ lib LibC fun ttyname_r(fd : Int, buf : Char*, buffersize : SizeT) : Int fun lchown(file : Char*, owner : UidT, group : GidT) : Int fun link(from : Char*, to : Char*) : Int - fun lockf(fd : Int, cmd : Int, len : OffT) : Int - fun lseek(fd : Int, offset : OffT, whence : Int) : OffT + fun lockf = lockf64(fd : Int, cmd : Int, len : OffT) : Int + fun lseek = lseek64(fd : Int, offset : OffT, whence : Int) : OffT fun pipe(pipedes : StaticArray(Int, 2)) : Int fun read(fd : Int, buf : Void*, nbytes : SizeT) : SSizeT - fun pread(x0 : Int, x1 : Void*, x2 : SizeT, x3 : OffT) : SSizeT + fun pread = pread64(x0 : Int, x1 : Void*, x2 : SizeT, x3 : OffT) : SSizeT fun rmdir(path : Char*) : Int fun symlink(from : Char*, to : Char*) : Int fun readlink(path : Char*, buf : Char*, size : SizeT) : SSizeT diff --git a/src/lib_c/i386-linux-musl/c/elf.cr b/src/lib_c/i386-linux-musl/c/elf.cr new file mode 100644 index 000000000000..32a0dc3152d1 --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt32T + alias Elf_Off = UInt32T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Word # Segment size in file + memsz : Elf_Word # Segment size in memory + flags : Elf_Word # Segment flags + align : Elf_Word # Segment alignment + end +end diff --git a/src/lib_c/i386-linux-musl/c/link.cr b/src/lib_c/i386-linux-musl/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/i386-linux-musl/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-darwin/c/stdlib.cr b/src/lib_c/x86_64-darwin/c/stdlib.cr index d3325e2defd0..e7d1e35d2ecb 100644 --- a/src/lib_c/x86_64-darwin/c/stdlib.cr +++ b/src/lib_c/x86_64-darwin/c/stdlib.cr @@ -17,7 +17,7 @@ lib LibC fun mkstemps(x0 : Char*, x1 : Int) : Int fun putenv(x0 : Char*) : Int fun realloc(x0 : Void*, x1 : SizeT) : Void* - fun realpath(x0 : Char*, x1 : Char*) : Char* + fun realpath = "realpath$DARWIN_EXTSN"(x0 : Char*, x1 : Char*) : Char* fun setenv(x0 : Char*, x1 : Char*, x2 : Int) : Int fun strtof(x0 : Char*, x1 : Char**) : Float fun strtod(x0 : Char*, x1 : Char**) : Double diff --git a/src/lib_c/x86_64-dragonfly/c/elf.cr b/src/lib_c/x86_64-dragonfly/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/x86_64-dragonfly/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/x86_64-dragonfly/c/link.cr b/src/lib_c/x86_64-dragonfly/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/x86_64-dragonfly/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-freebsd/c/elf.cr b/src/lib_c/x86_64-freebsd/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/x86_64-freebsd/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/x86_64-freebsd/c/link.cr b/src/lib_c/x86_64-freebsd/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/x86_64-freebsd/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-linux-gnu/c/elf.cr b/src/lib_c/x86_64-linux-gnu/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/x86_64-linux-gnu/c/link.cr b/src/lib_c/x86_64-linux-gnu/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/x86_64-linux-gnu/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-linux-musl/c/elf.cr b/src/lib_c/x86_64-linux-musl/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/x86_64-linux-musl/c/link.cr b/src/lib_c/x86_64-linux-musl/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/x86_64-linux-musl/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-netbsd/c/arpa/inet.cr b/src/lib_c/x86_64-netbsd/c/arpa/inet.cr new file mode 100644 index 000000000000..afac8795f66f --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/arpa/inet.cr @@ -0,0 +1,9 @@ +require "../netinet/in" +require "../stdint" + +lib LibC + fun htons(x0 : UInt16T) : UInt16T + fun ntohs(x0 : UInt16T) : UInt16T + fun inet_ntop(x0 : Int, x1 : Void*, x2 : Char*, x3 : SocklenT) : Char* + fun inet_pton(x0 : Int, x1 : Char*, x2 : Void*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/dirent.cr b/src/lib_c/x86_64-netbsd/c/dirent.cr new file mode 100644 index 000000000000..4b3186ab6a97 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/dirent.cr @@ -0,0 +1,30 @@ +require "./sys/types" + +lib LibC + struct DIR + dd_fd : Int + dd_loc : Long + dd_size : Long + dd_buf : Char* + dd_len : Int + dd_seek : OffT + dd_internal : Void* + dd_flags : Int + dd_lock : Void* + end + + DT_DIR = 4 + + struct Dirent + d_fileno : InoT + d_reclen : UInt16 + d_namlen : UInt16 + d_type : UInt8 + d_name : StaticArray(Char, 512) + end + + fun closedir(x0 : DIR*) : Int + fun opendir = __opendir30(x0 : Char*) : DIR* + fun readdir = __readdir30(x0 : DIR*) : Dirent* + fun rewinddir(x0 : DIR*) : Void +end diff --git a/src/lib_c/x86_64-netbsd/c/dlfcn.cr b/src/lib_c/x86_64-netbsd/c/dlfcn.cr new file mode 100644 index 000000000000..cbdf854f1912 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/dlfcn.cr @@ -0,0 +1,21 @@ +lib LibC + RTLD_LAZY = 1 + RTLD_NOW = 2 + RTLD_GLOBAL = 0x100 + RTLD_LOCAL = 0x200 + RTLD_NEXT = Pointer(Void).new(-1) + RTLD_DEFAULT = Pointer(Void).new(-2) + + struct DlInfo + dli_fname : Char* + dli_fbase : Void* + dli_sname : Char* + dli_saddr : Void* + end + + fun dlclose(x0 : Void*) : Int + fun dlerror : Char* + fun dlopen(x0 : Char*, x1 : Int) : Void* + fun dlsym(x0 : Void*, x1 : Char*) : Void* + fun dladdr(x0 : Void*, x1 : DlInfo*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/elf.cr b/src/lib_c/x86_64-netbsd/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/x86_64-netbsd/c/errno.cr b/src/lib_c/x86_64-netbsd/c/errno.cr new file mode 100644 index 000000000000..43eaeed4f02c --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/errno.cr @@ -0,0 +1,99 @@ +lib LibC + EPERM = 1 # Operation not permitted + ENOENT = 2 # No such file or directory + ESRCH = 3 # No such process + EINTR = 4 # Interrupted system call + EIO = 5 # Input/output error + ENXIO = 6 # Device not configured + E2BIG = 7 # Argument list too long + ENOEXEC = 8 # Exec format error + EBADF = 9 # Bad file descriptor + ECHILD = 10 # No child processes + EDEADLK = 11 # Resource deadlock avoided + ENOMEM = 12 # Cannot allocate memory + EACCES = 13 # Permission denied + EFAULT = 14 # Bad address + ENOTBLK = 15 # Block device required + EBUSY = 16 # Device busy + EEXIST = 17 # File exists + EXDEV = 18 # Cross-device link + ENODEV = 19 # Operation not supported by device + ENOTDIR = 20 # Not a directory + EISDIR = 21 # Is a directory + EINVAL = 22 # Invalid argument + ENFILE = 23 # Too many open files in system + EMFILE = 24 # Too many open files + ENOTTY = 25 # Inappropriate ioctl for device + ETXTBSY = 26 # Text file busy + EFBIG = 27 # File too large + ENOSPC = 28 # No space left on device + ESPIPE = 29 # Illegal seek + EROFS = 30 # Read-only file system + EMLINK = 31 # Too many links + EPIPE = 32 # Broken pipe + EDOM = 33 # Numerical argument out of domain + ERANGE = 34 # Result too large or too small + EAGAIN = 35 # Resource temporarily unavailable + EWOULDBLOCK = EAGAIN # Operation would block + EINPROGRESS = 36 # Operation now in progress + EALREADY = 37 # Operation already in progress + ENOTSOCK = 38 # Socket operation on non-socket + EDESTADDRREQ = 39 # Destination address required + EMSGSIZE = 40 # Message too long + EPROTOTYPE = 41 # Protocol wrong type for socket + ENOPROTOOPT = 42 # Protocol option not available + EPROTONOSUPPORT = 43 # Protocol not supported + ESOCKTNOSUPPORT = 44 # Socket type not supported + EOPNOTSUPP = 45 # Operation not supported + EPFNOSUPPORT = 46 # Protocol family not supported + EAFNOSUPPORT = 47 # Address family not supported by protocol family + EADDRINUSE = 48 # Address already in use + EADDRNOTAVAIL = 49 # Can't assign requested address + ENETDOWN = 50 # Network is down + ENETUNREACH = 51 # Network is unreachable + ENETRESET = 52 # Network dropped connection on reset + ECONNABORTED = 53 # Software caused connection abort + ECONNRESET = 54 # Connection reset by peer + ENOBUFS = 55 # No buffer space available + EISCONN = 56 # Socket is already connected + ENOTCONN = 57 # Socket is not connected + ESHUTDOWN = 58 # Can't send after socket shutdown + ETOOMANYREFS = 59 # Too many references: can't splice + ETIMEDOUT = 60 # Operation timed out + ECONNREFUSED = 61 # Connection refused + ELOOP = 62 # Too many levels of symbolic links + ENAMETOOLONG = 63 # File name too long + EHOSTDOWN = 64 # Host is down + EHOSTUNREACH = 65 # No route to host + ENOTEMPTY = 66 # Directory not empty + EPROCLIM = 67 # Too many processes + EUSERS = 68 # Too many users + EDQUOT = 69 # Disc quota exceeded + ESTALE = 70 # Stale NFS file handle + EREMOTE = 71 # Too many levels of remote in path + EBADRPC = 72 # RPC struct is bad + ERPCMISMATCH = 73 # RPC version wrong + EPROGUNAVAIL = 74 # RPC prog. not avail + EPROGMISMATCH = 75 # Program version wrong + EPROCUNAVAIL = 76 # Bad procedure for program + ENOLCK = 77 # No locks available + ENOSYS = 78 # Function not implemented + EFTYPE = 79 # Inappropriate file type or format + EAUTH = 80 # Authentication error + ENEEDAUTH = 81 # Need authenticator + EIDRM = 82 # Identifier removed + ENOMSG = 83 # No message of desired type + EOVERFLOW = 84 # Value too large to be stored in data type + EILSEQ = 85 # Illegal byte sequence + ENOTSUP = 86 # Not supported + ECANCELED = 87 # Operation canceled + EBADMSG = 88 # Bad or Corrupt message + ENODATA = 89 # No message available + ENOSR = 90 # No STREAM resources + ENOSTR = 91 # Not a STREAM + ETIME = 92 # STREAM ioctl timeout + ENOATTR = 93 # Attribute not found + EMULTIHOP = 94 # Multihop attempted + ENOLINK = 95 # Link has been severed + EPROTO = 96 # Protocol error +end diff --git a/src/lib_c/x86_64-netbsd/c/fcntl.cr b/src/lib_c/x86_64-netbsd/c/fcntl.cr new file mode 100644 index 000000000000..76ff615bef36 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/fcntl.cr @@ -0,0 +1,32 @@ +require "./sys/types" +require "./sys/stat" +require "./unistd" + +lib LibC + F_GETFD = 1 + F_SETFD = 2 + F_GETFL = 3 + F_SETFL = 4 + FD_CLOEXEC = 1 + O_CLOEXEC = 0x400000 + O_CREAT = 0x0200 + O_NOFOLLOW = 0x0100 + O_TRUNC = 0x0400 + O_APPEND = 0x0008 + O_NONBLOCK = 0x0004 + O_SYNC = 0x0080 + O_RDONLY = 0x0000 + O_RDWR = 0x0002 + O_WRONLY = 0x0001 + + struct Flock + l_start : OffT + l_len : OffT + l_pid : PidT + l_type : Short + l_whence : Short + end + + fun fcntl(x0 : Int, x1 : Int, ...) : Int + fun open(x0 : Char*, x1 : Int, ...) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/grp.cr b/src/lib_c/x86_64-netbsd/c/grp.cr new file mode 100644 index 000000000000..2fc351260b20 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/grp.cr @@ -0,0 +1,11 @@ +lib LibC + struct Group + gr_name : Char* + gr_passwd : Char* + gr_gid : GidT + gr_mem : Char** + end + + fun getgrnam_r(name : Char*, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int + fun getgrgid_r(gid : GidT, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/iconv.cr b/src/lib_c/x86_64-netbsd/c/iconv.cr new file mode 100644 index 000000000000..6814db786946 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/iconv.cr @@ -0,0 +1,9 @@ +require "./stddef" + +lib LibC + type IconvT = Void* + + fun iconv(cd : IconvT, inbuf : Char**, inbytesleft : SizeT*, outbuf : Char**, outbytesleft : SizeT*) : SizeT + fun iconv_close(cd : IconvT) : Int + fun iconv_open(tocode : Char*, fromcode : Char*) : IconvT +end diff --git a/src/lib_c/x86_64-netbsd/c/link.cr b/src/lib_c/x86_64-netbsd/c/link.cr new file mode 100644 index 000000000000..eb469a885889 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/link.cr @@ -0,0 +1,17 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + adds : UInt64 + subs : UInt64 + tls_modid : SizeT + tls_data : Void* + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-netbsd/c/netdb.cr b/src/lib_c/x86_64-netbsd/c/netdb.cr new file mode 100644 index 000000000000..4443325cd487 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/netdb.cr @@ -0,0 +1,37 @@ +require "./netinet/in" +require "./sys/socket" +require "./stdint" + +lib LibC + AI_PASSIVE = 0x1 + AI_CANONNAME = 0x2 + AI_NUMERICHOST = 0x4 + AI_NUMERICSERV = 0x8 + AI_ADDRCONFIG = 0x400 + EAI_AGAIN = 2 + EAI_BADFLAGS = 3 + EAI_FAIL = 4 + EAI_FAMILY = 5 + EAI_MEMORY = 6 + EAI_NONAME = 8 + EAI_SERVICE = 9 + EAI_SOCKTYPE = 10 + EAI_SYSTEM = 11 + EAI_OVERFLOW = 14 + + struct Addrinfo + ai_flags : Int + ai_family : Int + ai_socktype : Int + ai_protocol : Int + ai_addrlen : SocklenT + ai_canonname : Char* + ai_addr : Void* + ai_next : Addrinfo* + end + + fun freeaddrinfo(x0 : Addrinfo*) : Void + fun gai_strerror(x0 : Int) : Char* + fun getaddrinfo(x0 : Char*, x1 : Char*, x2 : Addrinfo*, x3 : Addrinfo**) : Int + fun getnameinfo(x0 : Void*, x1 : SocklenT, x2 : Char*, x3 : SizeT, x4 : Char*, x5 : SizeT, x6 : Int) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/netinet/in.cr b/src/lib_c/x86_64-netbsd/c/netinet/in.cr new file mode 100644 index 000000000000..c400a8cec4d2 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/netinet/in.cr @@ -0,0 +1,70 @@ +require "../sys/socket" +require "../stdint" + +lib LibC + IPPROTO_IP = 0 + IPPROTO_IPV6 = 41 + IPPROTO_ICMP = 1 + IPPROTO_RAW = 255 + IPPROTO_TCP = 6 + IPPROTO_UDP = 17 + + alias InPortT = UInt16 + alias InAddrT = UInt32 + + struct InAddr + s_addr : InAddrT + end + + union In6AddrU6Addr + __u6_addr8 : StaticArray(UInt8T, 16) + __u6_addr16 : StaticArray(UInt16T, 8) + __u6_addr32 : StaticArray(UInt32T, 4) + end + + struct In6Addr + __u6_addr : In6AddrU6Addr + end + + struct SockaddrIn + sin_len : UInt8T + sin_family : SaFamilyT + sin_port : InPortT + sin_addr : InAddr + sin_zero : StaticArray(Int8, 8) + end + + struct SockaddrIn6 + sin6_len : UInt8T + sin6_family : SaFamilyT + sin6_port : InPortT + sin6_flowinfo : UInt32T + sin6_addr : In6Addr + sin6_scope_id : UInt32T + end + + IP_MULTICAST_IF = 9 + IPV6_MULTICAST_IF = 9 + + IP_MULTICAST_TTL = 10 + IPV6_MULTICAST_HOPS = 10 + + IP_MULTICAST_LOOP = 11 + IPV6_MULTICAST_LOOP = 11 + + IP_ADD_MEMBERSHIP = 12 + IPV6_JOIN_GROUP = 12 + + IP_DROP_MEMBERSHIP = 13 + IPV6_LEAVE_GROUP = 13 + + struct IpMreq + imr_multiaddr : InAddr + imr_interface : InAddr + end + + struct Ipv6Mreq + ipv6mr_multiaddr : In6Addr + ipv6mr_interface : UInt + end +end diff --git a/src/lib_c/x86_64-netbsd/c/netinet/tcp.cr b/src/lib_c/x86_64-netbsd/c/netinet/tcp.cr new file mode 100644 index 000000000000..3f164c117c68 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/netinet/tcp.cr @@ -0,0 +1,5 @@ +lib LibC + TCP_NODELAY = 1 + TCP_KEEPINTVL = 5 + TCP_KEEPCNT = 6 +end diff --git a/src/lib_c/x86_64-netbsd/c/pthread.cr b/src/lib_c/x86_64-netbsd/c/pthread.cr new file mode 100644 index 000000000000..133d4922c57f --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/pthread.cr @@ -0,0 +1,39 @@ +require "./sys/types" + +@[Link("pthread")] +lib LibC + PTHREAD_MUTEX_ERRORCHECK = 1 + + fun pthread_attr_destroy(attr : PthreadAttrT*) : Int + fun pthread_attr_get_np(x0 : PthreadT, x1 : PthreadAttrT*) : Int + fun pthread_attr_getstack(addr : PthreadAttrT*, stackaddr : Void**, stacksize : SizeT*) : Int + fun pthread_attr_init(attr : PthreadAttrT*) : Int + fun pthread_condattr_destroy(x0 : PthreadCondattrT*) : Int + fun pthread_condattr_init(x0 : PthreadCondattrT*) : Int + fun pthread_condattr_setclock(x0 : PthreadCondattrT*, x1 : ClockidT) : Int + fun pthread_cond_broadcast(x0 : PthreadCondT*) : Int + fun pthread_cond_destroy(x0 : PthreadCondT*) : Int + fun pthread_cond_init(x0 : PthreadCondT*, x1 : PthreadCondattrT*) : Int + fun pthread_cond_signal(x0 : PthreadCondT*) : Int + fun pthread_cond_timedwait(x0 : PthreadCondT*, x1 : PthreadMutexT*, x2 : Timespec*) : Int + fun pthread_cond_wait(x0 : PthreadCondT*, x1 : PthreadMutexT*) : Int + fun pthread_create(x0 : PthreadT*, x1 : PthreadAttrT*, x2 : Void* -> Void*, x3 : Void*) : Int + fun pthread_detach(x0 : PthreadT) : Int + fun pthread_getattr_np(thread : PthreadT, attr : PthreadAttrT*) : Int + fun pthread_equal(x0 : PthreadT, x1 : PthreadT) : Int + fun pthread_getspecific(PthreadKeyT) : Void* + fun pthread_join(x0 : PthreadT, x1 : Void**) : Int + alias PthreadKeyDestructor = (Void*) -> + fun pthread_key_create(PthreadKeyT*, PthreadKeyDestructor) : Int + fun pthread_main_np : Int + fun pthread_mutexattr_destroy(x0 : PthreadMutexattrT*) : Int + fun pthread_mutexattr_init(x0 : PthreadMutexattrT*) : Int + fun pthread_mutexattr_settype(x0 : PthreadMutexattrT*, x1 : Int) : Int + fun pthread_mutex_destroy(x0 : PthreadMutexT*) : Int + fun pthread_mutex_init(x0 : PthreadMutexT*, x1 : PthreadMutexattrT*) : Int + fun pthread_mutex_lock(x0 : PthreadMutexT*) : Int + fun pthread_mutex_trylock(x0 : PthreadMutexT*) : Int + fun pthread_mutex_unlock(x0 : PthreadMutexT*) : Int + fun pthread_self : PthreadT + fun pthread_setspecific(PthreadKeyT, Void*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/pwd.cr b/src/lib_c/x86_64-netbsd/c/pwd.cr new file mode 100644 index 000000000000..250e8397f709 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/pwd.cr @@ -0,0 +1,17 @@ +lib LibC + struct Passwd + pw_name : Char* + pw_passwd : Char* + pw_uid : UidT + pw_gid : GidT + pw_change : TimeT + pw_class : Char* + pw_gecos : Char* + pw_dir : Char* + pw_shell : Char* + pw_expire : TimeT + end + + fun getpwnam_r = __getpwnam_r50(login : Char*, pwstore : Passwd*, buf : Char*, bufsize : SizeT, result : Passwd**) : Int + fun getpwuid_r = __getpwuid_r50(uid : UidT, pwstore : Passwd*, buf : Char*, bufsize : SizeT, result : Passwd**) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sched.cr b/src/lib_c/x86_64-netbsd/c/sched.cr new file mode 100644 index 000000000000..9d83ed18504e --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sched.cr @@ -0,0 +1,3 @@ +lib LibC + fun sched_yield : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/signal.cr b/src/lib_c/x86_64-netbsd/c/signal.cr new file mode 100644 index 000000000000..a9d866834f17 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/signal.cr @@ -0,0 +1,55 @@ +require "./sys/types" +require "./time" + +lib LibC + SIGHUP = 1 + SIGINT = 2 + SIGQUIT = 3 + SIGILL = 4 + SIGTRAP = 5 + SIGIOT = LibC::SIGABRT + SIGABRT = 6 + SIGEMT = 7 + SIGFPE = 8 + SIGKILL = 9 + SIGBUS = 10 + SIGSEGV = 11 + SIGSYS = 12 + SIGPIPE = 13 + SIGALRM = 14 + SIGTERM = 15 + SIGURG = 16 + SIGSTOP = 17 + SIGTSTP = 18 + SIGCONT = 19 + SIGCHLD = 20 + SIGTTIN = 21 + SIGTTOU = 22 + SIGIO = 23 + SIGXCPU = 24 + SIGXFSZ = 25 + SIGVTALRM = 26 + SIGUSR1 = 30 + SIGUSR2 = 31 + SIGINFO = 29 + SIGWINCH = 28 + + SIG_SETMASK = 3 + + alias SighandlerT = Int -> + SIG_DFL = SighandlerT.new(Pointer(Void).new(0_u64), Pointer(Void).null) + SIG_IGN = SighandlerT.new(Pointer(Void).new(1_u64), Pointer(Void).null) + + struct SigsetT + bits : UInt32[4] + end + + fun kill(x0 : PidT, x1 : Int) : Int + fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int + fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void + fun sigemptyset = __sigemptyset14(SigsetT*) : Int + fun sigfillset = __sigfillset14(SigsetT*) : Int + fun sigaddset = __sigaddset14(SigsetT*, Int) : Int + fun sigdelset = __sigdelset14(SigsetT*, Int) : Int + fun sigismember = __sigismember14(SigsetT*, Int) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/stdarg.cr b/src/lib_c/x86_64-netbsd/c/stdarg.cr new file mode 100644 index 000000000000..fcad7714f16a --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/stdarg.cr @@ -0,0 +1,10 @@ +lib LibC + struct VaListTag + gp_offset : UInt + fp_offset : UInt + overflow_arg_area : Void* + reg_save_area : Void* + end + + type VaList = VaListTag[1] +end diff --git a/src/lib_c/x86_64-netbsd/c/stddef.cr b/src/lib_c/x86_64-netbsd/c/stddef.cr new file mode 100644 index 000000000000..4afcdf34d723 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/stddef.cr @@ -0,0 +1,3 @@ +lib LibC + alias SizeT = ULong +end diff --git a/src/lib_c/x86_64-netbsd/c/stdint.cr b/src/lib_c/x86_64-netbsd/c/stdint.cr new file mode 100644 index 000000000000..5cb258eccd2c --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/stdint.cr @@ -0,0 +1,10 @@ +lib LibC + alias Int8T = SChar + alias Int16T = Short + alias Int32T = Int + alias Int64T = LongLong + alias UInt8T = Char + alias UInt16T = UShort + alias UInt32T = UInt + alias UInt64T = ULongLong +end diff --git a/src/lib_c/x86_64-netbsd/c/stdio.cr b/src/lib_c/x86_64-netbsd/c/stdio.cr new file mode 100644 index 000000000000..44fb5b8a1e74 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/stdio.cr @@ -0,0 +1,9 @@ +require "./sys/types" +require "./stddef" + +lib LibC + fun printf(x0 : Char*, ...) : Int + fun dprintf(fd : Int, format : Char*, ...) : Int + fun rename(x0 : Char*, x1 : Char*) : Int + fun snprintf(x0 : Char*, x1 : SizeT, x2 : Char*, ...) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/stdlib.cr b/src/lib_c/x86_64-netbsd/c/stdlib.cr new file mode 100644 index 000000000000..49aada507901 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/stdlib.cr @@ -0,0 +1,27 @@ +require "./stddef" +require "./sys/wait" + +lib LibC + struct DivT + quot : Int + rem : Int + end + + fun arc4random : UInt32 + fun arc4random_buf(x0 : Void*, x1 : SizeT) : Void + fun atof(x0 : Char*) : Double + fun div(x0 : Int, x1 : Int) : DivT + fun exit(x0 : Int) : NoReturn + fun free(x0 : Void*) : Void + fun getenv(x0 : Char*) : Char* + fun malloc(x0 : SizeT) : Void* + fun mkstemp(x0 : Char*) : Int + fun mkstemps(x0 : Char*, x1 : Int) : Int + fun putenv(x0 : Char*) : Int + fun realloc(x0 : Void*, x1 : SizeT) : Void* + fun realpath(x0 : Char*, x1 : Char*) : Char* + fun setenv(x0 : Char*, x1 : Char*, x2 : Int) : Int + fun strtof(x0 : Char*, x1 : Char**) : Float + fun strtod(x0 : Char*, x1 : Char**) : Double + fun unsetenv = __unsetenv13(x0 : Char*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/string.cr b/src/lib_c/x86_64-netbsd/c/string.cr new file mode 100644 index 000000000000..471d1ed82b36 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/string.cr @@ -0,0 +1,9 @@ +require "./stddef" + +lib LibC + fun memchr(x0 : Void*, c : Int, n : SizeT) : Void* + fun memcmp(x0 : Void*, x1 : Void*, x2 : SizeT) : Int + fun strcmp(x0 : Char*, x1 : Char*) : Int + fun strerror(x0 : Int) : Char* + fun strlen(x0 : Char*) : ULong +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/file.cr b/src/lib_c/x86_64-netbsd/c/sys/file.cr new file mode 100644 index 000000000000..e2bd3fd4b816 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/file.cr @@ -0,0 +1,11 @@ +lib LibC + @[Flags] + enum FlockOp + SH = 0x1 + EX = 0x2 + NB = 0x4 + UN = 0x8 + end + + fun flock(fd : Int, op : FlockOp) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/mman.cr b/src/lib_c/x86_64-netbsd/c/sys/mman.cr new file mode 100644 index 000000000000..2c6675659c2f --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/mman.cr @@ -0,0 +1,30 @@ +require "./types" + +lib LibC + PROT_NONE = 0x00 + PROT_READ = 0x01 + PROT_WRITE = 0x02 + PROT_EXEC = 0x04 + MAP_SHARED = 0x0001 + MAP_PRIVATE = 0x0002 + MAP_FIXED = 0x0010 + MAP_ANON = 0x1000 + MAP_ANONYMOUS = LibC::MAP_ANON + MAP_FAILED = Pointer(Void).new(-1) + MAP_STACK = 0x2000 + POSIX_MADV_NORMAL = 0 + POSIX_MADV_RANDOM = 1 + POSIX_MADV_SEQUENTIAL = 2 + POSIX_MADV_WILLNEED = 3 + POSIX_MADV_DONTNEED = 4 + MADV_NORMAL = LibC::POSIX_MADV_NORMAL + MADV_RANDOM = LibC::POSIX_MADV_RANDOM + MADV_SEQUENTIAL = LibC::POSIX_MADV_SEQUENTIAL + MADV_WILLNEED = LibC::POSIX_MADV_WILLNEED + MADV_DONTNEED = LibC::POSIX_MADV_DONTNEED + + fun mmap(x0 : Void*, x1 : SizeT, x2 : Int, x3 : Int, x4 : Int, x5 : OffT) : Void* + fun mprotect(x0 : Void*, x1 : SizeT, x2 : Int) : Int + fun munmap(x0 : Void*, x1 : SizeT) : Int + fun madvise(x0 : Void*, x1 : SizeT, x2 : Int) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/resource.cr b/src/lib_c/x86_64-netbsd/c/sys/resource.cr new file mode 100644 index 000000000000..d52182f69bce --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/resource.cr @@ -0,0 +1,25 @@ +lib LibC + struct RUsage + ru_utime : Timeval + ru_stime : Timeval + ru_maxrss : Long + ru_ixrss : Long + ru_idrss : Long + ru_isrss : Long + ru_minflt : Long + ru_majflt : Long + ru_nswap : Long + ru_inblock : Long + ru_oublock : Long + ru_msgsnd : Long + ru_msgrcv : Long + ru_nsignals : Long + ru_nvcsw : Long + ru_nivcsw : Long + end + + RUSAGE_SELF = 0 + RUSAGE_CHILDREN = -1 + + fun getrusage(who : Int, usage : RUsage*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/select.cr b/src/lib_c/x86_64-netbsd/c/sys/select.cr new file mode 100644 index 000000000000..cbbd58604883 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/select.cr @@ -0,0 +1,12 @@ +require "./types" +require "./time" +require "../time" +require "../signal" + +lib LibC + struct FdSet + fds_bits : StaticArray(UInt32T, 8) + end + + fun select(x0 : Int, x1 : FdSet*, x2 : FdSet*, x3 : FdSet*, x4 : Timeval*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/socket.cr b/src/lib_c/x86_64-netbsd/c/sys/socket.cr new file mode 100644 index 000000000000..d96f245bc42a --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/socket.cr @@ -0,0 +1,68 @@ +require "./types" + +lib LibC + SOCK_STREAM = 1 + SOCK_DGRAM = 2 + SOCK_RAW = 3 + SOCK_SEQPACKET = 5 + SOL_SOCKET = 0xffff + SO_BROADCAST = 0x0020 + SO_KEEPALIVE = 0x0008 + SO_LINGER = 0x0080 + SO_RCVBUF = 0x1002 + SO_REUSEADDR = 0x0004 + SO_REUSEPORT = 0x0200 + SO_SNDBUF = 0x1001 + PF_INET = LibC::AF_INET + PF_INET6 = LibC::AF_INET6 + PF_UNIX = LibC::PF_LOCAL + PF_UNSPEC = LibC::AF_UNSPEC + PF_LOCAL = LibC::AF_LOCAL + AF_INET = 2 + AF_INET6 = 24 + AF_UNIX = LibC::AF_LOCAL + AF_UNSPEC = 0 + AF_LOCAL = 1 + SHUT_RD = 0 + SHUT_WR = 1 + SHUT_RDWR = 2 + SOCK_CLOEXEC = 0x10000000 + + alias SocklenT = UInt + alias SaFamilyT = UInt8 + + struct Sockaddr + sa_len : UInt8 + sa_family : SaFamilyT + sa_data : StaticArray(Char, 14) + end + + struct SockaddrStorage + ss_len : UInt8 + ss_family : SaFamilyT + __ss_pad1 : StaticArray(Char, 6) + __ss_pad2 : UInt64 + __ss_pad3 : StaticArray(Char, 240) + end + + struct Linger + l_onoff : Int + l_linger : Int + end + + fun accept(x0 : Int, x1 : Sockaddr*, x2 : SocklenT*) : Int + fun bind(x0 : Int, x1 : Sockaddr*, x2 : SocklenT) : Int + fun connect(x0 : Int, x1 : Sockaddr*, x2 : SocklenT) : Int + fun getpeername(x0 : Int, x1 : Sockaddr*, x2 : SocklenT*) : Int + fun getsockname(x0 : Int, x1 : Sockaddr*, x2 : SocklenT*) : Int + fun getsockopt(x0 : Int, x1 : Int, x2 : Int, x3 : Void*, x4 : SocklenT*) : Int + fun listen(x0 : Int, x1 : Int) : Int + fun recv(x0 : Int, x1 : Void*, x2 : SizeT, x3 : Int) : SSizeT + fun recvfrom(x0 : Int, x1 : Void*, x2 : SizeT, x3 : Int, x4 : Sockaddr*, x5 : SocklenT*) : SSizeT + fun send(x0 : Int, x1 : Void*, x2 : SizeT, x3 : Int) : SSizeT + fun sendto(x0 : Int, x1 : Void*, x2 : SizeT, x3 : Int, x4 : Sockaddr*, x5 : SocklenT) : SSizeT + fun setsockopt(x0 : Int, x1 : Int, x2 : Int, x3 : Void*, x4 : SocklenT) : Int + fun shutdown(x0 : Int, x1 : Int) : Int + fun socket = __socket30(x0 : Int, x1 : Int, x2 : Int) : Int + fun socketpair(x0 : Int, x1 : Int, x2 : Int, x3 : Int*) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/stat.cr b/src/lib_c/x86_64-netbsd/c/sys/stat.cr new file mode 100644 index 000000000000..68c44a28df9e --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/stat.cr @@ -0,0 +1,57 @@ +require "./types" +require "../time" + +lib LibC + S_IFMT = 0o170000 + S_IFBLK = 0o060000 + S_IFCHR = 0o020000 + S_IFIFO = 0o010000 + S_IFREG = 0o100000 + S_IFDIR = 0o040000 + S_IFLNK = 0o120000 + S_IFSOCK = 0o140000 + S_IRUSR = 0o000400 + S_IWUSR = 0o000200 + S_IXUSR = 0o000100 + S_IRWXU = 0o000700 + S_IRGRP = 0o000040 + S_IWGRP = 0o000020 + S_IXGRP = 0o000010 + S_IRWXG = 0o000070 + S_IROTH = 0o000004 + S_IWOTH = 0o000002 + S_IXOTH = 0o000001 + S_IRWXO = 0o000007 + S_ISUID = 0o004000 + S_ISGID = 0o002000 + S_ISVTX = 0o001000 + + struct Stat + st_dev : DevT + st_mode : ModeT + st_ino : InoT + st_nlink : NlinkT + st_uid : UidT + st_gid : GidT + st_rdev : DevT + st_atim : Timespec + st_mtim : Timespec + st_ctim : Timespec + st_birthtim : Timespec + st_size : OffT + st_blocks : BlkcntT + st_blksize : BlksizeT + st_flags : UInt32T + st_gen : UInt32T + st_spare : UInt32[2] + end + + fun chmod(x0 : Char*, x1 : ModeT) : Int + fun fstat = __fstat50(x0 : Int, x1 : Stat*) : Int + fun lstat = __lstat50(x0 : Char*, x1 : Stat*) : Int + fun mkdir(x0 : Char*, x1 : ModeT) : Int + fun mkfifo(x0 : Char*, x1 : ModeT) : Int + fun mknod = __mknod50(x0 : Char*, x1 : ModeT, x2 : DevT) : Int + fun stat = __stat50(x0 : Char*, x1 : Stat*) : Int + fun umask(x0 : ModeT) : ModeT +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/time.cr b/src/lib_c/x86_64-netbsd/c/sys/time.cr new file mode 100644 index 000000000000..1677150d4851 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/time.cr @@ -0,0 +1,16 @@ +require "./types" + +lib LibC + struct Timeval + tv_sec : TimeT + tv_usec : SusecondsT + end + + struct Timezone + tz_minuteswest : Int + tz_dsttime : Int + end + + fun gettimeofday = __gettimeofday50(x0 : Timeval*, x1 : Timezone*) : Int + fun utimes = __utimes50(path : Char*, times : Timeval[2]) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/types.cr b/src/lib_c/x86_64-netbsd/c/sys/types.cr new file mode 100644 index 000000000000..198bfb681218 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/types.cr @@ -0,0 +1,67 @@ +require "../stddef" +require "../stdint" + +lib LibC + alias BlkcntT = Int64 + alias BlksizeT = Int32 + alias ClockT = UInt + alias ClockidT = Int + alias DevT = UInt64 + alias GidT = UInt32 + alias IdT = UInt32 + alias InoT = UInt64 + alias ModeT = UInt32 + alias NlinkT = UInt32 + alias OffT = Int64 + alias PidT = Int32 + + type PthreadT = Void* + + struct PthreadAttrT + pta_magic : UInt + pta_flags : Int + pta_private : Void* + end + + struct PthreadQueueT + ptqh_first : Void* + ptqh_last : Void* + end + + struct PthreadCondT + ptc_magic : UInt + ptc_lock : UInt8 + ptc_waiters : PthreadQueueT + ptc_mutex : Void* + ptc_private : Void* + end + + struct PthreadCondattrT + ptca_magic : UInt + ptca_private : Void* + end + + type PthreadKeyT = Int + + struct PthreadMutexT + ptm_magic : UInt + ptm_errorcheck : UInt8 + ptm_pad1 : UInt8[3] + ptm_ceiling : UInt8 + ptm_pad2 : UInt8[2] + ptm_owner : PthreadT + ptm_waiters : PthreadT* + ptm_recursed : UInt + ptm_spare2 : Void* + end + + struct PthreadMutexattrT + ptma_magic : UInt + ptma_private : Void* + end + + alias SSizeT = Long + alias SusecondsT = UInt + alias TimeT = Int64 + alias UidT = UInt32 +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/un.cr b/src/lib_c/x86_64-netbsd/c/sys/un.cr new file mode 100644 index 000000000000..476b28c6ed57 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/un.cr @@ -0,0 +1,9 @@ +require "./socket" + +lib LibC + struct SockaddrUn + sun_len : UInt8 + sun_family : SaFamilyT + sun_path : StaticArray(Char, 104) + end +end diff --git a/src/lib_c/x86_64-netbsd/c/sys/wait.cr b/src/lib_c/x86_64-netbsd/c/sys/wait.cr new file mode 100644 index 000000000000..e9c185b5ac9c --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sys/wait.cr @@ -0,0 +1,8 @@ +require "./types" +require "../signal" + +lib LibC + WNOHANG = 1 + + fun waitpid(x0 : PidT, x1 : Int*, x2 : Int) : PidT +end diff --git a/src/lib_c/x86_64-netbsd/c/sysctl.cr b/src/lib_c/x86_64-netbsd/c/sysctl.cr new file mode 100644 index 000000000000..c9102ff0a7b3 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/sysctl.cr @@ -0,0 +1,13 @@ +lib LibC + CTL_HW = 6 + HW_NCPU = 3 + + CTL_KERN = 1 + KERN_PROC = 14 + KERN_PROC_ARGS = 48 + KERN_PROC_PATHNAME = 5 + + PATH_MAX = 1024 + + fun sysctl(name : Int*, namelen : UInt, oldp : Void*, oldlenp : SizeT*, newp : Void*, newlen : SizeT) : Int +end diff --git a/src/lib_c/x86_64-netbsd/c/termios.cr b/src/lib_c/x86_64-netbsd/c/termios.cr new file mode 100644 index 000000000000..f788b9bd41f1 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/termios.cr @@ -0,0 +1,95 @@ +require "./sys/types" + +lib LibC + VEOF = 0 + VEOL = 1 + VERASE = 3 + VINTR = 8 + VKILL = 5 + VMIN = 16 + VQUIT = 9 + VSTART = 12 + VSTOP = 13 + VSUSP = 10 + BRKINT = 0x00000002 + ICRNL = 0x00000100 + IGNBRK = 0x00000001 + IGNCR = 0x00000080 + IGNPAR = 0x00000004 + INLCR = 0x00000040 + INPCK = 0x00000010 + ISTRIP = 0x00000020 + IXANY = 0x00000800 + IXOFF = 0x00000400 + IXON = 0x00000200 + PARMRK = 0x00000008 + OPOST = 0x00000001 + ONLCR = 0x00000002 + OCRNL = 0x00000010 + ONOCR = 0x00000040 + ONLRET = 0x00000080 + B0 = 0 + B50 = 50 + B75 = 75 + B110 = 110 + B134 = 134 + B150 = 150 + B200 = 200 + B300 = 300 + B600 = 600 + B1200 = 1200 + B1800 = 1800 + B2400 = 2400 + B4800 = 4800 + B9600 = 9600 + B19200 = 19200 + B38400 = 38400 + CSIZE = 0x00000300 + CS5 = 0x00000000 + CS6 = 0x00000100 + CS7 = 0x00000200 + CS8 = 0x00000300 + CSTOPB = 0x00000400 + CREAD = 0x00000800 + PARENB = 0x00001000 + PARODD = 0x00002000 + HUPCL = 0x00004000 + CLOCAL = 0x00008000 + ECHO = 0x00000008 + ECHOE = 0x00000002 + ECHOK = 0x00000004 + ECHONL = 0x00000010 + ICANON = 0x00000100 + IEXTEN = 0x00000400 + ISIG = 0x00000080 + NOFLSH = 0x80000000 + TOSTOP = 0x00400000 + TCSANOW = 0 + TCSADRAIN = 1 + TCSAFLUSH = 2 + TCIFLUSH = 1 + TCIOFLUSH = 3 + TCOFLUSH = 2 + TCIOFF = 3 + TCION = 4 + TCOOFF = 1 + TCOON = 2 + + alias CcT = Char + alias SpeedT = UInt + alias TcflagT = UInt + + struct Termios + c_iflag : TcflagT + c_oflag : TcflagT + c_cflag : TcflagT + c_lflag : TcflagT + c_cc : StaticArray(CcT, 20) + c_ispeed : Int + c_ospeed : Int + end + + fun tcgetattr(x0 : Int, x1 : Termios*) : Int + fun tcsetattr(x0 : Int, x1 : Int, x2 : Termios*) : Int + fun cfmakeraw(x0 : Termios*) : Void +end diff --git a/src/lib_c/x86_64-netbsd/c/time.cr b/src/lib_c/x86_64-netbsd/c/time.cr new file mode 100644 index 000000000000..17fb6b2dcaa6 --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/time.cr @@ -0,0 +1,37 @@ +require "./sys/types" + +lib LibC + CLOCK_REALTIME = 0 + CLOCK_MONOTONIC = 3 + + struct Tm + tm_sec : Int + tm_min : Int + tm_hour : Int + tm_mday : Int + tm_mon : Int + tm_year : Int + tm_wday : Int + tm_yday : Int + tm_isdst : Int + tm_gmtoff : Long + tm_zone : Char* + end + + struct Timespec + tv_sec : TimeT + tv_nsec : Long + end + + fun clock_gettime = __clock_gettime50(x0 : ClockidT, x1 : Timespec*) : Int + fun clock_settime = __clock_settime50(x0 : ClockidT, x1 : Timespec*) : Int + fun gmtime_r = __gmtime_r50(x0 : TimeT*, x1 : Tm*) : Tm* + fun localtime_r = __localtime_r50(x0 : TimeT*, x1 : Tm*) : Tm* + fun mktime = __mktime50(x0 : Tm*) : TimeT + fun tzset : Void + fun timegm = __timegm50(x0 : Tm*) : TimeT + + $daylight : Int + $timezone : Long + $tzname : StaticArray(Char*, 2) +end diff --git a/src/lib_c/x86_64-netbsd/c/unistd.cr b/src/lib_c/x86_64-netbsd/c/unistd.cr new file mode 100644 index 000000000000..89c28c5d3f9f --- /dev/null +++ b/src/lib_c/x86_64-netbsd/c/unistd.cr @@ -0,0 +1,46 @@ +require "./sys/types" +require "./stdint" + +lib LibC + F_OK = 0 + X_OK = 0x01 + W_OK = 0x02 + R_OK = 0x04 + SC_CLK_TCK = 39 + SC_PAGESIZE = 28 + + fun chroot(dirname : Char*) : Int + fun access(x0 : Char*, x1 : Int) : Int + fun chdir(x0 : Char*) : Int + fun chown = __posix_chown(x0 : Char*, x1 : UidT, x2 : GidT) : Int + fun close(x0 : Int) : Int + fun dup2(x0 : Int, x1 : Int) : Int + fun _exit(x0 : Int) : NoReturn + fun execvp(x0 : Char*, x1 : Char**) : Int + fun fdatasync(x0 : Int) : Int + @[ReturnsTwice] + fun fork : PidT + fun fsync(x0 : Int) : Int + fun ftruncate(x0 : Int, x1 : OffT) : Int + fun getcwd(x0 : Char*, x1 : SizeT) : Char* + fun gethostname(x0 : Char*, x1 : SizeT) : Int + fun getpgid(x0 : PidT) : PidT + fun getpid : PidT + fun getppid : PidT + fun isatty(x0 : Int) : Int + fun ttyname_r(fd : Int, buf : Char*, buffersize : SizeT) : Int + fun lchown = __posix_lchown(x0 : Char*, x1 : UidT, x2 : GidT) : Int + fun link(x0 : Char*, x1 : Char*) : Int + fun lockf(x0 : Int, x1 : Int, x2 : OffT) : Int + fun lseek(x0 : Int, x1 : OffT, x2 : Int) : OffT + fun pipe(x0 : Int*) : Int + fun read(x0 : Int, x1 : Void*, x2 : SizeT) : SSizeT + fun pread(x0 : Int, x1 : Void*, x2 : SizeT, x3 : OffT) : SSizeT + fun rmdir(x0 : Char*) : Int + fun symlink(x0 : Char*, x1 : Char*) : Int + fun readlink(path : Char*, buf : Char*, size : SizeT) : SSizeT + fun syscall(x0 : Int, ...) : Int + fun sysconf(x0 : Int) : Long + fun unlink(x0 : Char*) : Int + fun write(x0 : Int, x1 : Void*, x2 : SizeT) : SSizeT +end diff --git a/src/lib_c/x86_64-openbsd/c/elf.cr b/src/lib_c/x86_64-openbsd/c/elf.cr new file mode 100644 index 000000000000..f9ccc3a115ab --- /dev/null +++ b/src/lib_c/x86_64-openbsd/c/elf.cr @@ -0,0 +1,24 @@ +require "./sys/types" + +lib LibC + alias Elf_Half = UInt16T + alias Elf_Word = UInt32T + alias Elf_Sword = Int32T + alias Elf_Xword = UInt64T + alias Elf_Sxword = Int64T + alias Elf_Addr = UInt64T + alias Elf_Off = UInt64T + alias Elf_Section = UInt16T + alias Elf_Versym = Elf_Half + + struct Elf_Phdr + type : Elf_Word # Segment type + flags : Elf_Word # Segment flags + offset : Elf_Off # Segment file offset + vaddr : Elf_Addr # Segment virtual address + paddr : Elf_Addr # Segment physical address + filesz : Elf_Xword # Segment size in file + memsz : Elf_Xword # Segment size in memory + align : Elf_Xword # Segment alignment + end +end diff --git a/src/lib_c/x86_64-openbsd/c/link.cr b/src/lib_c/x86_64-openbsd/c/link.cr new file mode 100644 index 000000000000..a29bd3030a42 --- /dev/null +++ b/src/lib_c/x86_64-openbsd/c/link.cr @@ -0,0 +1,13 @@ +require "./elf" + +lib LibC + struct DlPhdrInfo + addr : Elf_Addr + name : Char* + phdr : Elf_Phdr* + phnum : Elf_Half + end + + alias DlPhdrCallback = (DlPhdrInfo*, LibC::SizeT, Void*) -> LibC::Int + fun dl_iterate_phdr(callback : DlPhdrCallback, data : Void*) +end diff --git a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr new file mode 100644 index 000000000000..44cfeab11261 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr @@ -0,0 +1,5 @@ +require "c/winnt" + +lib LibC + fun GetConsoleMode(hConsoleHandle : HANDLE, lpMode : DWORD*) : BOOL +end diff --git a/src/lib_c/x86_64-windows-msvc/c/fileapi.cr b/src/lib_c/x86_64-windows-msvc/c/fileapi.cr index 72e05dd8b7ed..1b7ef08ce276 100644 --- a/src/lib_c/x86_64-windows-msvc/c/fileapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/fileapi.cr @@ -32,6 +32,7 @@ lib LibC OPEN_EXISTING = 3 + FILE_ATTRIBUTE_NORMAL = 0x80 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 FILE_SHARE_READ = 0x1 @@ -79,4 +80,7 @@ lib LibC fun FindFirstFileW(lpFileName : LPWSTR, lpFindFileData : WIN32_FIND_DATAW*) : HANDLE fun FindNextFileW(hFindFile : HANDLE, lpFindFileData : WIN32_FIND_DATAW*) : BOOL fun FindClose(hFindFile : HANDLE) : BOOL + + fun SetFileTime(hFile : HANDLE, lpCreationTime : FILETIME*, + lpLastAccessTime : FILETIME*, lpLastWriteTime : FILETIME*) : BOOL end diff --git a/src/lib_c/x86_64-windows-msvc/c/io.cr b/src/lib_c/x86_64-windows-msvc/c/io.cr index 0dd6d93bf7e0..dc3dfd345523 100644 --- a/src/lib_c/x86_64-windows-msvc/c/io.cr +++ b/src/lib_c/x86_64-windows-msvc/c/io.cr @@ -15,4 +15,5 @@ lib LibC fun _get_osfhandle(fd : Int) : IntPtrT fun _pipe(pfds : Int*, psize : UInt, textmode : Int) : Int fun _dup2(fd1 : Int, fd2 : Int) : Int + fun _commit(fd : Int) : Int end diff --git a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr index 476c54af3028..6f5ed78f865d 100644 --- a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr @@ -1,6 +1,8 @@ require "./basetsd" lib LibC + CREATE_UNICODE_ENVIRONMENT = 0x00000400 + struct PROCESS_INFORMATION hProcess : HANDLE hThread : HANDLE diff --git a/src/lib_c/x86_64-windows-msvc/c/winnt.cr b/src/lib_c/x86_64-windows-msvc/c/winnt.cr index 2d6b9e8f2869..e60e00064273 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr @@ -16,7 +16,8 @@ lib LibC FILE_ATTRIBUTE_READONLY = 0x1 FILE_ATTRIBUTE_REPARSE_POINT = 0x400 - FILE_READ_ATTRIBUTES = 0x80 + FILE_READ_ATTRIBUTES = 0x80 + FILE_WRITE_ATTRIBUTES = 0x0100 # Memory protection constants PAGE_READWRITE = 0x04 diff --git a/src/llvm/abi.cr b/src/llvm/abi.cr index 323269741b37..fffcdf29ad64 100644 --- a/src/llvm/abi.cr +++ b/src/llvm/abi.cr @@ -1,3 +1,4 @@ +# Based on https://github.com/rust-lang/rust/blob/29ac04402d53d358a1f6200bea45a301ff05b2d1/src/librustc_trans/trans/cabi.rs abstract class LLVM::ABI getter target_data : TargetData getter? osx : Bool diff --git a/src/llvm/abi/aarch64.cr b/src/llvm/abi/aarch64.cr index 52cf84ae4b82..14a576d415e8 100644 --- a/src/llvm/abi/aarch64.cr +++ b/src/llvm/abi/aarch64.cr @@ -136,7 +136,7 @@ class LLVM::ABI::AArch64 < LLVM::ABI end ArgType.direct(aty, cast) else - ArgType.indirect(aty, LLVM::Attribute::ByVal) + ArgType.indirect(aty, nil) end end end diff --git a/src/llvm/abi/x86.cr b/src/llvm/abi/x86.cr index 64976863a47c..abc67c4a0471 100644 --- a/src/llvm/abi/x86.cr +++ b/src/llvm/abi/x86.cr @@ -1,5 +1,6 @@ require "../abi" +# Based on https://github.com/rust-lang/rust/blob/29ac04402d53d358a1f6200bea45a301ff05b2d1/src/librustc_trans/trans/cabi_x86.rs class LLVM::ABI::X86 < LLVM::ABI def abi_info(atys : Array(Type), rty : Type, ret_def : Bool, context : Context) ret_ty = compute_return_type(rty, ret_def, context) diff --git a/src/llvm/abi/x86_64.cr b/src/llvm/abi/x86_64.cr index 8248b3c218fc..cb12d74a3364 100644 --- a/src/llvm/abi/x86_64.cr +++ b/src/llvm/abi/x86_64.cr @@ -1,5 +1,6 @@ require "../abi" +# Based on https://github.com/rust-lang/rust/blob/29ac04402d53d358a1f6200bea45a301ff05b2d1/src/librustc_trans/trans/cabi_x86_64.rs class LLVM::ABI::X86_64 < LLVM::ABI def abi_info(atys : Array(Type), rty : Type, ret_def : Bool, context : Context) arg_tys = Array(LLVM::Type).new(atys.size) diff --git a/src/llvm/abi/x86_win64.cr b/src/llvm/abi/x86_win64.cr new file mode 100644 index 000000000000..69e9b0823ba9 --- /dev/null +++ b/src/llvm/abi/x86_win64.cr @@ -0,0 +1,22 @@ +require "../abi" + +# Based on https://github.com/rust-lang/rust/blob/29ac04402d53d358a1f6200bea45a301ff05b2d1/src/librustc_trans/trans/cabi_x86_win64.rs +class LLVM::ABI::X86_Win64 < LLVM::ABI::X86 + private def compute_arg_types(atys, context) + atys.map do |t| + case t.kind + when Type::Kind::Struct + size = target_data.abi_size(t) + case size + when 1 then ArgType.direct(t, context.int8) + when 2 then ArgType.direct(t, context.int16) + when 4 then ArgType.direct(t, context.int32) + when 8 then ArgType.direct(t, context.int64) + else ArgType.indirect(t, nil) + end + else + non_struct(t, context) + end + end + end +end diff --git a/src/llvm/ext/llvm_ext.cc b/src/llvm/ext/llvm_ext.cc index b418dabb5716..e67716e0fc46 100644 --- a/src/llvm/ext/llvm_ext.cc +++ b/src/llvm/ext/llvm_ext.cc @@ -9,6 +9,11 @@ #include #include #include +#include +#include +#include +#include +#include using namespace llvm; @@ -21,6 +26,12 @@ using namespace llvm; #define LLVM_VERSION_LE(major, minor) \ (LLVM_VERSION_MAJOR < (major) || LLVM_VERSION_MAJOR == (major) && LLVM_VERSION_MINOR <= (minor)) +#if LLVM_VERSION_GE(7, 0) +#include +#else +#include +#endif + #if LLVM_VERSION_GE(6, 0) #include #endif @@ -475,10 +486,90 @@ char *LLVMExtBasicBlockName(LLVMBasicBlockRef BB) { #endif } +static TargetMachine *unwrap(LLVMTargetMachineRef P) { + return reinterpret_cast(P); +} + +void LLVMExtTargetMachineEnableGlobalIsel(LLVMTargetMachineRef T, LLVMBool Enable) { +#if LLVM_VERSION_GE(7, 0) + unwrap(T)->setGlobalISel(Enable); +#endif +} + +// Copy paste of https://github.com/llvm/llvm-project/blob/dace8224f38a31636a02fe9c2af742222831f70c/llvm/lib/ExecutionEngine/ExecutionEngineBindings.cpp#L160-L214 +// but with a parameter to set global isel state +LLVMBool LLVMExtCreateMCJITCompilerForModule( + LLVMExecutionEngineRef *OutJIT, LLVMModuleRef M, + LLVMMCJITCompilerOptions *PassedOptions, size_t SizeOfPassedOptions, + LLVMBool EnableGlobalISel, + char **OutError) { + LLVMMCJITCompilerOptions options; + // If the user passed a larger sized options struct, then they were compiled + // against a newer LLVM. Tell them that something is wrong. + if (SizeOfPassedOptions > sizeof(options)) { + *OutError = strdup( + "Refusing to use options struct that is larger than my own; assuming " + "LLVM library mismatch."); + return 1; + } + + + // Defend against the user having an old version of the API by ensuring that + // any fields they didn't see are cleared. We must defend against fields being + // set to the bitwise equivalent of zero, and assume that this means "do the + // default" as if that option hadn't been available. + LLVMInitializeMCJITCompilerOptions(&options, sizeof(options)); + memcpy(&options, PassedOptions, SizeOfPassedOptions); + + + TargetOptions targetOptions; + targetOptions.EnableFastISel = options.EnableFastISel; + #if LLVM_VERSION_GE(7, 0) + targetOptions.EnableGlobalISel = EnableGlobalISel; + #endif + std::unique_ptr Mod(unwrap(M)); + + if (Mod) + // Set function attribute "frame-pointer" based on + // NoFramePointerElim. + for (auto &F : *Mod) { + auto Attrs = F.getAttributes(); + StringRef Value = options.NoFramePointerElim ? "all" : "none"; + Attrs = Attrs.addAttribute(F.getContext(), AttributeList::FunctionIndex, + "frame-pointer", Value); + F.setAttributes(Attrs); + } + + + std::string Error; + EngineBuilder builder(std::move(Mod)); + builder.setEngineKind(EngineKind::JIT) + .setErrorStr(&Error) + .setOptLevel((CodeGenOpt::Level)options.OptLevel) + .setTargetOptions(targetOptions); + bool JIT; + if (Optional CM = unwrap(options.CodeModel, JIT)) + builder.setCodeModel(*CM); + if (options.MCJMM) + builder.setMCJITMemoryManager( + std::unique_ptr(unwrap(options.MCJMM))); + + TargetMachine* tm = builder.selectTarget(); + #if LLVM_VERSION_GE(7, 0) + tm->setGlobalISel(EnableGlobalISel); + #endif + + if (ExecutionEngine *JIT = builder.create(tm)) { + *OutJIT = wrap(JIT); + return 0; + } + *OutError = strdup(Error.c_str()); + return 1; +} + LLVMMetadataRef LLVMExtDIBuilderGetOrCreateArraySubrange( DIBuilderRef Dref, uint64_t Lo, uint64_t Count) { return wrap(Dref->getOrCreateSubrange(Lo, Count)); -} - + } } diff --git a/src/llvm/jit_compiler.cr b/src/llvm/jit_compiler.cr index 8fb45068b993..b881e06ee8d7 100644 --- a/src/llvm/jit_compiler.cr +++ b/src/llvm/jit_compiler.cr @@ -5,7 +5,7 @@ class LLVM::JITCompiler mod.take_ownership { raise "Can't create two JIT compilers for the same module" } # if LibLLVM.create_jit_compiler_for_module(out @unwrap, mod, 3, out error) != 0 - if LibLLVM.create_mc_jit_compiler_for_module(out @unwrap, mod, nil, 0, out error) != 0 + if LibLLVMExt.create_mc_jit_compiler_for_module(out @unwrap, mod, nil, 0, false, out error) != 0 raise LLVM.string_and_dispose(error) end diff --git a/src/llvm/lib_llvm_ext.cr b/src/llvm/lib_llvm_ext.cr index 07227aa2907c..7c29cf3f6c2a 100644 --- a/src/llvm/lib_llvm_ext.cr +++ b/src/llvm/lib_llvm_ext.cr @@ -1,5 +1,9 @@ require "./lib_llvm" -@[Link(ldflags: "#{__DIR__}/ext/llvm_ext.o")] +{% if flag?(:win32) %} + @[Link(ldflags: "#{__DIR__}/ext/llvm_ext.obj")] +{% else %} + @[Link(ldflags: "#{__DIR__}/ext/llvm_ext.o")] +{% end %} lib LibLLVMExt alias Char = LibC::Char alias Int = LibC::Int @@ -158,4 +162,7 @@ lib LibLLVMExt fun normalize_target_triple = LLVMExtNormalizeTargetTriple(triple : Char*) : Char* fun basic_block_name = LLVMExtBasicBlockName(basic_block : LibLLVM::BasicBlockRef) : Char* fun di_builder_get_or_create_array_subrange = LLVMExtDIBuilderGetOrCreateArraySubrange(builder : DIBuilder, lo : UInt64, count : UInt64) : Metadata + + fun target_machine_enable_global_isel = LLVMExtTargetMachineEnableGlobalIsel(machine : LibLLVM::TargetMachineRef, enable : Bool) + fun create_mc_jit_compiler_for_module = LLVMExtCreateMCJITCompilerForModule(jit : LibLLVM::ExecutionEngineRef*, m : LibLLVM::ModuleRef, options : LibLLVM::JITCompilerOptions*, options_length : UInt32, enable_global_isel : Bool, error : UInt8**) : Int32 end diff --git a/src/llvm/target_machine.cr b/src/llvm/target_machine.cr index e9c8b793aa7f..b2b214f53892 100644 --- a/src/llvm/target_machine.cr +++ b/src/llvm/target_machine.cr @@ -32,6 +32,10 @@ class LLVM::TargetMachine emit_to_file llvm_mod, filename, LLVM::CodeGenFileType::AssemblyFile end + def enable_global_isel=(enable : Bool) + LibLLVMExt.target_machine_enable_global_isel(self, enable) + end + private def emit_to_file(llvm_mod, filename, type) status = LibLLVM.target_machine_emit_to_file(self, llvm_mod, filename, type, out error_msg) unless status == 0 @@ -43,6 +47,8 @@ class LLVM::TargetMachine def abi triple = self.triple case triple + when /x86_64.+windows-msvc/ + ABI::X86_Win64.new(self) when /x86_64|amd64/ ABI::X86_64.new(self) when /i386|i486|i586|i686/ diff --git a/src/log.cr b/src/log.cr index bbbc9f8dc493..bebc82cc6dc1 100644 --- a/src/log.cr +++ b/src/log.cr @@ -12,10 +12,17 @@ # Log.info { "Program started" } # ``` # +# Data can be associated with a log entry via the `Log::Emitter` yielded in the logging methods. +# +# ``` +# Log.info &.emit("User logged in", user_id: 42) +# ``` +# # If you want to log an exception, you can indicate it in the `exception:` named argument. # # ``` # Log.warn(exception: e) { "Oh no!" } +# Log.warn exception: e, &.emit("Oh no!", user_id: 42) # ``` # # The block is only evaluated if the current message is to be emitted to some `Log::Backend`. @@ -73,6 +80,8 @@ # # If you need to change the default level, backend or sources call `Log.setup` upon startup. # +# NOTE: Calling `setup` will override previous `setup` calls. +# # ``` # Log.setup(:debug) # Log debug and above for all sources to STDOUT # Log.setup("myapp.*, http.*", :notice) # Log notice and above for myapp.* and http.* sources only, and log nothing for any other source. @@ -98,10 +107,10 @@ # sources errors (or higher) to an elasticsearch backend. # # ``` -# Log.setup |c| +# Log.setup do |c| # backend = Log::IOBackend.new # -# c.bind "*", :warning, backend +# c.bind "*", :warn, backend # c.bind "db.*", :debug, backend # c.bind "*", :error, ElasticSearchBackend.new("http://localhost:9200") # end @@ -138,12 +147,14 @@ end require "./log/backend" require "./log/broadcast_backend" require "./log/builder" -require "./log/context" +require "./log/metadata" require "./log/entry" +require "./log/format" require "./log/main" require "./log/setup" require "./log/log" require "./log/memory_backend" require "./log/io_backend" +require "./log/dispatch" Log.setup diff --git a/src/log/backend.cr b/src/log/backend.cr index 5fd394596f4a..8be7c1974fac 100644 --- a/src/log/backend.cr +++ b/src/log/backend.cr @@ -2,12 +2,25 @@ require "crystal/datum" # Base class for all backends. abstract class Log::Backend + property dispatcher : Dispatcher + + def initialize(dispatch_mode : DispatchMode = :async) + @dispatcher = Dispatcher.for(dispatch_mode) + end + + def initialize(@dispatcher : Dispatcher) + end + # Writes the *entry* to this backend. abstract def write(entry : Entry) # Closes underlying resources used by this backend. def close + @dispatcher.close end -end -alias Log::Formatter = Entry, IO -> + # :nodoc: + def dispatch(entry : Entry) + @dispatcher.dispatch entry, self + end +end diff --git a/src/log/broadcast_backend.cr b/src/log/broadcast_backend.cr index d5a81c07f58e..eae027187e05 100644 --- a/src/log/broadcast_backend.cr +++ b/src/log/broadcast_backend.cr @@ -11,17 +11,22 @@ class Log::BroadcastBackend < Log::Backend @backends = Hash(Log::Backend, Severity).new + def initialize + super(:direct) + end + def append(backend : Log::Backend, level : Severity) @backends[backend] = level end def write(entry : Entry) @backends.each do |backend, level| - backend.write(entry) if (@level || level) <= entry.severity + backend.dispatch(entry) if (@level || level) <= entry.severity end end def close + @backends.each_key &.close end # :nodoc: diff --git a/src/log/builder.cr b/src/log/builder.cr index 99c1e7c4de68..f4b1dedda33c 100644 --- a/src/log/builder.cr +++ b/src/log/builder.cr @@ -143,6 +143,11 @@ class Log::Builder end end + # :nodoc: + def close + @bindings.each &.backend.close + end + # :nodoc: def self.matches(source : String, pattern : String) : Bool return true if source == pattern diff --git a/src/log/context.cr b/src/log/context.cr deleted file mode 100644 index 0361f87d594b..000000000000 --- a/src/log/context.cr +++ /dev/null @@ -1,70 +0,0 @@ -# Immutable structured context information for logging. -# -# See `Log.context`, `Log.context=`, `Log::Context#clear`, `Log::Context#set`, `Log.with_context`. -# -# NOTE: If you'd like to format the context as JSON, remember to `require "log/json"`. -class Log::Context - Crystal.datum types: {nil: Nil, bool: Bool, i: Int32, i64: Int64, f: Float32, f64: Float64, s: String, time: Time}, hash_key_type: String, immutable: true, target_type: Log::Context - - # Returns an empty `Log::Context`. - # - # NOTE: Since `Log::Context` is immutable, it's safe to share this instance. - class_getter empty : Log::Context = Log::Context.new - - # Creates an empty `Log::Context`. - def initialize - @raw = Hash(String, Context).new - end - - # Creates `Log::Context` from the given *tuple*. - def initialize(tuple : NamedTuple) - @raw = raw = Hash(String, Context).new - tuple.each do |key, value| - raw[key.to_s] = to_context(value) - end - end - - # Creates `Log::Context` from the given *hash*. - def initialize(hash : Hash(String, V)) forall V - @raw = raw = Hash(String, Context).new - hash.each do |key, value| - raw[key] = to_context(value) - end - end - - # Creates `Log::Context` from the given *hash*. - def initialize(hash : Hash(Symbol, V)) forall V - @raw = raw = Hash(String, Context).new - hash.each do |key, value| - raw[key.to_s] = to_context(value) - end - end - - # :nodoc: - def initialize(ary : Array) - @raw = ary.map { |e| to_context(e) } - end - - # Returns a new `Log::Context` with the keys and values of this context and *other* combined. - # A value in *other* takes precedence over the one in this context. - def merge(other : Context) - return other if self.object_id == @@empty.object_id - return self if other.object_id == @@empty.object_id - Context.new(self.as_h.merge(other.as_h).clone) - end - - private def to_context(value) - value.is_a?(Context) ? value : Context.new(value) - end -end - -class Fiber - # :nodoc: - getter logging_context : Log::Context { Log::Context.empty } - - # :nodoc: - def logging_context=(value : Log::Context) - raise ArgumentError.new "Expected hash context, not #{value.raw.class}" unless value.as_h? - @logging_context = value - end -end diff --git a/src/log/dispatch.cr b/src/log/dispatch.cr new file mode 100644 index 000000000000..1a186f32662f --- /dev/null +++ b/src/log/dispatch.cr @@ -0,0 +1,91 @@ +class Log + # Base interface implemented by log entry dispatchers + # + # Dispatchers are in charge of sending log entries according + # to different strategies. + module Dispatcher + alias Spec = Dispatcher | DispatchMode + + # Dispatch a log entry to the specified backend + abstract def dispatch(entry : Entry, backend : Backend) + + # Close the dispatcher, releasing resources + def close + end + + # :nodoc: + def self.for(mode : DispatchMode) + case mode + in .sync? + SyncDispatcher.new + in .async? + AsyncDispatcher.new + in .direct? + DirectDispatcher + end + end + end + + enum DispatchMode + Sync + Async + Direct + end + + # Stateless dispatcher that deliver log entries immediately + module DirectDispatcher + extend Dispatcher + + def self.dispatch(entry : Entry, backend : Backend) + backend.write(entry) + end + end + + # Deliver log entries asynchronously through a channels + class AsyncDispatcher + include Dispatcher + + def initialize(buffer_size = 2048) + @channel = Channel({Entry, Backend}).new(buffer_size) + @done = Channel(Nil).new + spawn write_logs + end + + def dispatch(entry : Entry, backend : Backend) + @channel.send({entry, backend}) + end + + private def write_logs + while msg = @channel.receive? + entry, backend = msg + backend.write(entry) + end + + @done.send nil + end + + def close + # TODO: this might fail if being closed from different threads + unless @channel.closed? + @channel.close + @done.receive + end + end + end + + # Deliver log entries directly. It uses a mutex to guarantee + # one entry is delivered at a time. + class SyncDispatcher + include Dispatcher + + def initialize + @mutex = Mutex.new(:unchecked) + end + + def dispatch(entry : Entry, backend : Backend) + @mutex.synchronize do + backend.write(entry) + end + end + end +end diff --git a/src/log/entry.cr b/src/log/entry.cr index 7ded5220fcf5..e9d7098faa94 100644 --- a/src/log/entry.cr +++ b/src/log/entry.cr @@ -9,7 +9,9 @@ enum Log::Severity # Used for normal but significant conditions. Notice # Used for conditions that can potentially cause application oddities, but that can be automatically recovered. - Warning + Warn + # DEPRECATED: Use `Warn`. + Warning = Warn # Used for any error that is fatal to the operation, but not to the service or application. Error # Used for any error that is forcing a shutdown of the service or application @@ -19,14 +21,14 @@ enum Log::Severity def label case self - when Trace then "TRACE" - when Debug then "DEBUG" - when Info then "INFO" - when Notice then "NOTICE" - when Warning then "WARNING" - when Error then "ERROR" - when Fatal then "FATAL" - when None then "NONE" + when Trace then "TRACE" + when Debug then "DEBUG" + when Info then "INFO" + when Notice then "NOTICE" + when Warn then "WARN" + when Error then "ERROR" + when Fatal then "FATAL" + when None then "NONE" else raise "unreachable" end @@ -37,10 +39,11 @@ struct Log::Entry getter source : String getter severity : Severity getter message : String - getter timestamp = Time.local - getter context = Log.context + getter timestamp : Time + getter context : Metadata = Log.context.metadata + getter data : Metadata getter exception : Exception? - def initialize(@source : String, @severity : Severity, @message : String, @exception : Exception?) + def initialize(@source : String, @severity : Severity, @message : String, @data : Log::Metadata, @exception : Exception?, *, @timestamp = Time.local) end end diff --git a/src/log/format.cr b/src/log/format.cr new file mode 100644 index 000000000000..baf56f6b165d --- /dev/null +++ b/src/log/format.cr @@ -0,0 +1,198 @@ +class Log + # The program name used for log entries + # + # Defaults to the executable name + class_property progname = File.basename(PROGRAM_NAME) + + # The current process PID + protected class_getter pid : String = Process.pid.to_s + + # Base interface to convert log entries and write them to an `IO` + module Formatter + # Writes a `Log::Entry` through an `IO` + abstract def format(entry : Log::Entry, io : IO) + + # Creates an instance of a `Log::Formatter` that calls + # the specified `Proc` for every entry + def self.new(&proc : (Log::Entry, IO) ->) + ProcFormatter.new proc + end + end + + # :nodoc: + private struct ProcFormatter + include Formatter + + def initialize(@proc : (Log::Entry, IO) ->) + end + + def format(entry : Log::Entry, io : IO) + @proc.call(entry, io) + end + end + + # Base implementation of `Log::Formatter` to convert + # log entries into text representation + # + # This can be used to create efficient formatters: + # ``` + # struct MyFormat < Log::StaticFormat + # def run + # string "- " + # severity + # string ": " + # message + # end + # end + # + # Log.setup(:info, Log::IOBackend.new(formatter: MyFormat)) + # Log.info { "Hello" } # => - INFO: Hello + # Log.error { "Oh, no!" } # => - ERROR: Oh, no! + # ``` + # + # There is also a helper macro to generate these formatters. Here's + # an example that generates the same result: + # ``` + # Log.define_formatter MyFormat, "- #{severity}: #{message}" + # ``` + abstract struct StaticFormatter + extend Formatter + + def initialize(@entry : Log::Entry, @io : IO) + end + + # Write the entry timestamp in RFC3339 format + def timestamp + @entry.timestamp.to_rfc3339(@io, fraction_digits: 6) + end + + # Write a fixed string + def string(str) + @io << str + end + + # Write the message of the entry + def message + @io << @entry.message + end + + # Write the severity + # + # This writes the severity in uppercase and left padded + # with enough space so all the severities fit + def severity + @entry.severity.label.rjust(@io, 6) + end + + # Write the source for non-root entries + # + # It doesn't write any output for entries generated from the root logger. + # Parameters `before` and `after` can be provided to be written around + # the value. + # ``` + # source(before: '[', after: ']') # => [http.server] + # ``` + def source(*, before = nil, after = nil) + if @entry.source.size > 0 + @io << before << @entry.source << after + end + end + + # Write all the values from the entry data + # + # It doesn't write any output if the entry data is empty. + # Parameters `before` and `after` can be provided to be written around + # the value. + def data(*, before = nil, after = nil) + unless @entry.data.empty? + @io << before << @entry.data << after + end + end + + # Write all the values from the context + # + # It doesn't write any output if the context is empty. + # Parameters `before` and `after` can be provided to be written around + # the value. + def context(*, before = nil, after = nil) + unless @entry.context.empty? + @io << before << @entry.context << after + end + end + + # Write the exception, including backtrace + # + # It doesn't write any output unless there is an exception in the entry. + # Parameters `before` and `after` can be provided to be written around + # the value. `before` defaults to `'\n'` so the exception is written + # on a separate line + def exception(*, before = '\n', after = nil) + if ex = @entry.exception + @io << before + ex.inspect_with_backtrace(@io) + @io << after + end + end + + # Write the program name. See `Log.progname`. + def progname + @io << Log.progname + end + + # Write the current process identifier + def pid(*, before = '#', after = nil) + @io << before << Log.pid << after + end + + # Write the `Log::Entry` to the `IO` using this pattern + def self.format(entry, io) + new(entry, io).run + end + + # Subclasses must implement this method to define the output pattern + abstract def run + end + + # Generate subclasses of `Log::StaticFormatter` from a string with interpolations + # + # Example: + # ``` + # Log.define_formatter MyFormat, "- #{severity}: #{message}" + # ``` + # See `Log::StaticFormatter` for the available methods that can + # be called within the interpolations. + macro define_formatter(name, pattern) + struct {{name}} < ::Log::StaticFormatter + def run + {% for part in pattern.expressions %} + {% if part.is_a?(StringLiteral) %} + string {{ part }} + {% else %} + {{ part }} + {% end %} + {% end %} + end + end + end +end + +# Default short format +# +# It writes log entries with the following format: +# ``` +# 2020-05-07T17:40:07.994508000Z INFO - my.source: Initializing everything +# ``` +# +# When the entries have context data it's also written to the output: +# ``` +# 2020-05-07T17:40:07.994508000Z INFO - my.source: Initializing everything -- {"data" => 123} +# ``` +# +# Exceptions are written in a separate line: +# ``` +# 2020-05-07T17:40:07.994508000Z ERROR - my.source: Something failed +# Oh, no (Exception) +# from ... +# ``` +Log.define_formatter Log::ShortFormat, "#{timestamp} #{severity} - #{source(after: ": ")}#{message}" \ + "#{data(before: " -- ")}#{context(before: " -- ")}#{exception}" diff --git a/src/log/io_backend.cr b/src/log/io_backend.cr index 1e5020353a34..b449444ba439 100644 --- a/src/log/io_backend.cr +++ b/src/log/io_backend.cr @@ -1,45 +1,28 @@ # A `Log::Backend` that emits to an `IO` (defaults to STDOUT). class Log::IOBackend < Log::Backend property io : IO - property progname : String - property formatter : Formatter? + property formatter : Formatter - def initialize(@io = STDOUT, @formatter = nil) - @mutex = Mutex.new(:unchecked) - @progname = File.basename(PROGRAM_NAME) - end + {% if flag?(:win32) %} + # TODO: this constructor must go away once channels are fixed in Windows + def initialize(@io = STDOUT, *, @formatter : Formatter = ShortFormat, dispatcher : Dispatcher::Spec = DispatchMode::Sync) + super(dispatcher) + end + {% else %} + def initialize(@io = STDOUT, *, @formatter : Formatter = ShortFormat, dispatcher : Dispatcher::Spec = DispatchMode::Async) + super(dispatcher) + end + {% end %} def write(entry : Entry) - @mutex.synchronize do - format(entry) - io.puts - io.flush - end + format(entry) + io.puts + io.flush end # Emits the *entry* to the given *io*. - # It will use the `#formatter` if defined, otherwise will call `#default_format`. + # It uses the `#formatter` to convert. def format(entry : Entry) - if formatter = @formatter - formatter.call(entry, io) - else - default_format(entry) - end - end - - # Emits the *entry* to the given *io*. - def default_format(entry : Entry) - label = entry.severity.label - io << label[0] << ", [" - entry.timestamp.to_rfc3339(io) - io << " #" << Process.pid << "] " - label.rjust(6, io) - io << " -- " << @progname << ":" << entry.source << ": " << entry.message - if entry.context.size > 0 - io << " -- " << entry.context - end - if ex = entry.exception - io << " -- " << ex.class << ": " << ex - end + @formatter.format(entry, io) end end diff --git a/src/log/json.cr b/src/log/json.cr index 3d2045ea9306..1d1bb26780ed 100644 --- a/src/log/json.cr +++ b/src/log/json.cr @@ -1,7 +1,7 @@ require "json" -class Log::Context - # Returns `Log::Context` as JSON value. +class Log::Metadata + # Returns `Log::Metadata` as JSON value. # # NOTE: `require "log/json"` is required to opt-in to this feature. # @@ -9,6 +9,21 @@ class Log::Context # log_entry.context.to_json # => "{\"user_id\":1}" # ``` def to_json(builder : JSON::Builder) : Nil - @raw.to_json builder + builder.object do + each do |(key, value)| + builder.field key.to_json_object_key do + value.to_json(builder) + end + end + end + end + + struct Value + # Returns `Log::Metadata::Value` as JSON value. + # + # NOTE: `require "log/json"` is required to opt-in to this feature. + def to_json(builder : JSON::Builder) : Nil + @raw.to_json builder + end end end diff --git a/src/log/log.cr b/src/log/log.cr index 6681d3fe093d..698081e8a0fa 100644 --- a/src/log/log.cr +++ b/src/log/log.cr @@ -38,21 +38,28 @@ class Log debug: Severity::Debug, info: Severity::Info, notice: Severity::Notice, - warn: Severity::Warning, + warn: Severity::Warn, error: Severity::Error, fatal: Severity::Fatal, } %} - # Logs a message if the logger's current severity is lower or equal to `{{severity}}`. def {{method.id}}(*, exception : Exception? = nil) - return unless backend = @backend severity = Severity.new({{severity}}) return unless level <= severity - entry = Log.with_context do - message = yield.to_s - Entry.new @source, severity, message, exception - end - backend.write entry + + return unless backend = @backend + + dsl = Emitter.new(@source, severity, exception) + result = yield dsl + entry = + case result + when Entry + result + else + dsl.emit(result.to_s) + end + + backend.dispatch entry end {% end %} end diff --git a/src/log/main.cr b/src/log/main.cr index 14937c17a1b8..18b9aee1a12a 100644 --- a/src/log/main.cr +++ b/src/log/main.cr @@ -38,14 +38,16 @@ class Log {% for method in %i(trace debug info notice warn error fatal) %} # See `Log#{{method.id}}`. def self.{{method.id}}(*, exception : Exception? = nil) - Top.{{method.id}}(exception: exception) do - yield + Top.{{method.id}}(exception: exception) do |dsl| + yield dsl end end {% end %} @@builder = Builder.new + at_exit { @@builder.close } + # Returns the default `Log::Builder` used for `Log.for` calls. def self.builder @@builder @@ -53,21 +55,29 @@ class Log # Returns the current fiber logging context. def self.context : Log::Context - Fiber.current.logging_context + Log::Context.new(Fiber.current.logging_context) end # Sets the current fiber logging context. - def self.context=(value : Log::Context) + def self.context=(value : Log::Metadata) Fiber.current.logging_context = value end + # :ditto: + def self.context=(value : Log::Context) + # NOTE: There is a need for `Metadata` and `Context` setters in + # because `Log.context` returns a `Log::Context` for allowing DSL like `Log.context.set(a: 1)` + # but if the metadata is built manually the construct `Log.context = metadata` will be used. + Log.context = value.metadata + end + # Returns the current fiber logging context. def context : Log::Context Log.context end # Sets the current fiber logging context. - def context=(value : Log::Context) + def context=(value : Log::Metadata | Log::Context) Log.context = value end @@ -98,7 +108,12 @@ class Log end end - class Context + struct Context + getter metadata : Metadata + + def initialize(@metadata : Metadata) + end + # Clears the current `Fiber` logging context. # # ``` @@ -106,7 +121,7 @@ class Log # Log.info { "message with empty context" } # ``` def clear - Fiber.current.logging_context = Log::Context.empty + Fiber.current.logging_context = @metadata = Log::Metadata.empty end # Extends the current `Fiber` logging context. @@ -114,36 +129,61 @@ class Log # ``` # Log.context.set a: 1 # Log.context.set b: 2 - # Log.info { %q(message with {"a" => 1, "b" => 2} context") } - # extra = {:c => "3"} - # Log.context.set extra - # Log.info { %q(message with {"a" => 1, "b" => 2, "c" => "3" } context) } - # extra = {"c" => 3} - # Log.context.set extra - # Log.info { %q(message with {"a" => 1, "b" => 2, "c" => 3 } context) } + # Log.info { %q(message with a: 1, b: 2 context") } + # h = {:c => "3"} + # Log.context.set extra: h + # Log.info { %q(message with a: 1, b: 2, extra: {"c" => "3"} context) } + # h = {"c" => 3} + # Log.context.set extra: h + # Log.info { %q(message with a: 1, b: 2, extra: {"c" => 3} context) } # ``` def set(**kwargs) - extend_fiber_context(Fiber.current, Log::Context.new(kwargs)) + extend_fiber_context(Fiber.current, kwargs) end # :ditto: - def set(values : Hash(String, V)) forall V - extend_fiber_context(Fiber.current, Log::Context.new(values)) + def set(values) + extend_fiber_context(Fiber.current, values) end - # :ditto: - def set(values : Hash(Symbol, V)) forall V - extend_fiber_context(Fiber.current, Log::Context.new(values)) + private def extend_fiber_context(fiber : Fiber, values) + context = fiber.logging_context + fiber.logging_context = @metadata = context.extend(values) end + end - # :ditto: - def set(values : NamedTuple) - extend_fiber_context(Fiber.current, Log::Context.new(values)) + # Helper DSL module for emitting log entries with data. + struct Emitter + # :nodoc: + def initialize(@source : String, @severity : Severity, @exception : Exception?) end - private def extend_fiber_context(fiber : Fiber, values : Context) - context = fiber.logging_context - fiber.logging_context = context.merge(values) + # Emits a logs entry with a message, and data attached to + # + # ``` + # Log.info &.emit("Program started") # No data, same as Log.info { "Program started" } + # Log.info &.emit("User logged in", user_id: 42) # With entry data + # Log.info &.emit(action: "Logged in", user_id: 42) # Empty string message, only data + # Log.error exception: ex, &.emit("Oopps", account: {id: 42}) # With data and exception + # ``` + def emit(message : String) : Entry + emit(message, Metadata.empty) + end + + def emit(message : String, **kwargs) : Entry + emit(message, kwargs) + end + + def emit(message : String, data : Metadata | Hash | NamedTuple) : Entry + Entry.new(@source, @severity, message, Metadata.build(data), @exception) + end + + def emit(**kwargs) : Entry + emit(kwargs) + end + + def emit(data : Metadata | Hash | NamedTuple) : Entry + emit("", Metadata.build(data)) end end end diff --git a/src/log/memory_backend.cr b/src/log/memory_backend.cr index 2e044ca1a8af..483f9aae0e51 100644 --- a/src/log/memory_backend.cr +++ b/src/log/memory_backend.cr @@ -3,6 +3,10 @@ class Log::MemoryBackend < Log::Backend getter entries = Array(Log::Entry).new + def initialize + super(:direct) + end + def write(entry : Log::Entry) @entries << entry end diff --git a/src/log/metadata.cr b/src/log/metadata.cr new file mode 100644 index 000000000000..b79d6dcbefec --- /dev/null +++ b/src/log/metadata.cr @@ -0,0 +1,226 @@ +# Immutable structured metadata information for logging. +# +# See `Log.context`, `Log.context=`, `Log::Context#clear`, `Log::Context#set`, `Log.with_context`, and `Log::Emitter`. +# +# NOTE: If you'd like to format the context as JSON, remember to `require "log/json"`. +class Log::Metadata + struct Value; end + + include Enumerable({Symbol, Log::Metadata::Value}) + alias Entry = {key: Symbol, value: Value} + + # Returns an empty `Log::Metadata`. + # + # NOTE: Since `Log::Metadata` is immutable, it's safe to share this instance. + class_getter empty : Log::Metadata = Log::Metadata.new + + @parent : Metadata? + # The maximum size this metadata would need. + # Initially is the parent.max_total_size + entries.size . + # When the metadata is defragmented max_total_size will be updated with size + protected getter max_total_size : Int32 + @max_total_size = uninitialized Int32 + # How many entries are potentially overriden from parent (ie: initial entries.size) + @overriden_size = uninitialized Int32 + # How many entries are stored from @first. + # Initially are @overriden_size, the one explictly overriden in entries argument. + # When the metadata is defragmented @size will be increased up to + # the actual number of entries resulting from merging the parent + @size = uninitialized Int32 + # @first needs to be the last ivar of Metadata. The entries are allocated together with self + @first = uninitialized Entry + + def self.new(parent : Metadata? = nil, entries : NamedTuple | Hash = NamedTuple.new) + data_size = instance_sizeof(self) + sizeof(Entry) * {entries.size + (parent.try(&.max_total_size) || 0) - 1, 0}.max + data = GC.malloc(data_size).as(self) + data.setup(parent, entries) + data + end + + protected def setup(@parent : Metadata?, entries : NamedTuple | Hash) + @size = @overriden_size = entries.size + @max_total_size = @size + (@parent.try(&.max_total_size) || 0) + ptr_entries = pointerof(@first) + + if entries.is_a?(NamedTuple) + entries.each_with_index do |key, value, i| + ptr_entries[i] = {key: key, value: Value.to_metadata_value(value)} + end + else + entries.each_with_index do |(key, value), i| + ptr_entries[i] = {key: key, value: Value.to_metadata_value(value)} + end + end + end + + # Returns a `Metadata` with the information of the argument. + # Used to handle `Log::Context#set` and `Log#Emitter.emit` overloads. + def self.build(value : NamedTuple | Hash) + return @@empty if value.empty? + Metadata.new(nil, value) + end + + # :ditto: + def self.build(value : Metadata) + value + end + + # Returns a `Log::Metadata` with all the entries of *self* + # and *other*. If a key is defined in both, the values in *other* are used. + def extend(other : NamedTuple | Hash) : Metadata + return Metadata.build(other) if self.empty? + return self if other.empty? + + Metadata.new(self, other) + end + + def empty? + parent = @parent + + @size == 0 && (parent.nil? || parent.empty?) + end + + # Removes the reference to *parent*. Flattening the entries from it into *self*. + # *self* was originally allocated with enough entries to perform this action. + # + # If multiple threads execute defrag concurrently, the entries + # will be recomputed, but the result should be the same. + # + # * @parent.nil? signals if the defrag is needed/done + # * The values of @overriden_size, pointerof(@first) are never changed + # * @parent is set at the very end of the method + protected def defrag + parent = @parent + return if parent.nil? + + total_size = @overriden_size + ptr_entries = pointerof(@first) + next_free_entry = ptr_entries + @overriden_size + + parent.each do |(key, value)| + overriden = false + @overriden_size.times do |i| + if ptr_entries[i][:key] == key + overriden = true + break + end + end + + unless overriden + next_free_entry.value = {key: key, value: value} + next_free_entry += 1 + total_size += 1 + end + end + + @size = total_size + @max_total_size = total_size + @parent = nil + end + + def each(& : {Symbol, Value} ->) + defrag + ptr_entries = pointerof(@first) + + @size.times do |i| + entry = ptr_entries[i] + yield({entry[:key], entry[:value]}) + end + end + + def [](key : Symbol) : Value + fetch(key) { raise KeyError.new "Missing metadata key: #{key.inspect}" } + end + + def []?(key : Symbol) : Value? + fetch(key) { nil } + end + + def fetch(key) + entry = find_entry(key) + entry ? entry[:value] : yield key + end + + protected def find_entry(key) : Entry? + # checking the @parent before @size ensures that if other + # thread is doing defrag, the results will be consistent + # without locking. + + parent = @parent + + ptr_entries = pointerof(@first) + @size.times do |i| + return ptr_entries[i] if ptr_entries[i][:key] == key + end + + return parent.find_entry(key) if parent + + nil + end + + def ==(other : Metadata) + self_kv = self.to_a + other_kv = other.to_a + + return false if self_kv.size != other_kv.size + + # sort kv tuples by key + self_kv.sort_by!(&.[0]) + other_kv.sort_by!(&.[0]) + + self_kv.each_with_index do |(key, value), i| + return false unless key == other_kv[i][0] && value == other_kv[i][1] + end + + true + end + + # :nodoc: + def ==(other) + false + end + + def to_s(io : IO) : Nil + found_one = false + each do |(key, value)| + io << ", " if found_one + io << key + io << ": " + value.inspect(io) + found_one = true + end + end + + struct Value + Crystal.datum types: {nil: Nil, bool: Bool, i: Int32, i64: Int64, f: Float32, f64: Float64, s: String, time: Time}, hash_key_type: String, immutable: false, target_type: Log::Metadata::Value + + # Creates `Log::Metadata` from the given *values*. + # All keys are converted to `String` + def initialize(hash : NamedTuple | Hash) + @raw = raw = Hash(String, Value).new + hash.each do |key, value| + raw[key.to_s] = Value.to_metadata_value(value) + end + end + + # :nodoc: + def initialize(ary : Array) + @raw = ary.map { |e| Value.to_metadata_value(e) } + end + + # :nodoc: + def self.to_metadata_value(value) + value.is_a?(Value) ? value : Value.new(value) + end + end +end + +class Fiber + # :nodoc: + getter logging_context : Log::Metadata { Log::Metadata.empty } + + # :nodoc: + def logging_context=(value : Log::Metadata) + @logging_context = value + end +end diff --git a/src/log/setup.cr b/src/log/setup.cr index 7371f8876b40..cb26c44d5962 100644 --- a/src/log/setup.cr +++ b/src/log/setup.cr @@ -26,6 +26,14 @@ class Log end end + @[Deprecated("Use default_level, default_sources named arguments")] + def self.setup_from_env(*, builder : Log::Builder = Log.builder, + level : String, + sources : String, + backend = Log::IOBackend.new) + Log.setup(sources, Log::Severity.parse(level), backend, builder: builder) + end + # Setups logging based on `LOG_LEVEL` environment variable. def self.setup_from_env(*, builder : Log::Builder = Log.builder, default_level : Log::Severity = Log::Severity::Info, diff --git a/src/logger.cr b/src/logger.cr deleted file mode 100644 index 99bb513c06ed..000000000000 --- a/src/logger.cr +++ /dev/null @@ -1,192 +0,0 @@ -# The `Logger` class provides a simple but sophisticated logging utility that you can use to output messages. -# -# The messages have associated levels, such as `INFO` or `ERROR` that indicate their importance. -# You can then give the `Logger` a level, and only messages at that level or higher will be printed. -# -# For instance, in a production system, you may have your `Logger` set to `INFO` or even `WARN`. -# When you are developing the system, however, you probably want to know about the program’s internal state, -# and would set the `Logger` to `DEBUG`. -# -# ### Example -# -# ``` -# require "logger" -# -# log = Logger.new(STDOUT) -# log.level = Logger::WARN -# -# # or -# log = Logger.new(STDOUT, level: Logger::WARN) -# -# log.debug("Created logger") -# log.info("Program started") -# log.warn("Nothing to do!") -# -# begin -# File.each_line("/foo/bar.log") do |line| -# unless line =~ /^(\w+) = (.*)$/ -# log.error("Line in wrong format: #{line}") -# end -# end -# rescue err -# log.fatal("Caught exception; exiting") -# log.fatal(err) -# end -# ``` -# -# If logging to multiple locations is required, an `IO::MultiWriter` can be -# used. -# -# ``` -# file = File.new("production.log", "a") -# writer = IO::MultiWriter.new(file, STDOUT) -# -# log = Logger.new(writer) -# log.level = Logger::DEBUG -# log.debug("Created logger") -# ``` -@[Deprecated("Use `Log` module instead")] -class Logger - property level : Severity - property progname : String - - # Customizable `Proc` (with a reasonable default) - # which the `Logger` uses to format and print its entries. - # - # Use this setter to provide a custom formatter. - # The `Logger` will invoke it with the following arguments: - # - severity: a `Logger::Severity` - # - datetime: `Time`, the entry's timestamp - # - progname: `String`, the program name, if set when the logger was built - # - message: `String`, the body of a message - # - io: `IO`, the Logger's stream, to which you must write the final output - # - # Example: - # - # ``` - # require "logger" - # - # logger = Logger.new(STDOUT) - # logger.progname = "YodaBot" - # - # logger.formatter = Logger::Formatter.new do |severity, datetime, progname, message, io| - # label = severity.unknown? ? "ANY" : severity.to_s - # io << label[0] << ", [" << datetime << " #" << Process.pid << "] " - # io << label.rjust(5) << " -- " << progname << ": " << message - # end - # - # logger.warn("Fear leads to anger. Anger leads to hate. Hate leads to suffering.") - # - # # Prints to the console: - # # "W, [2017-05-06 18:00:41 -0300 #11927] WARN -- - # # YodaBot: Fear leads to anger. Anger leads to hate. Hate leads to suffering." - # ``` - property formatter - - # A logger severity level. - enum Severity - # Low-level information for developers - DEBUG - - # Generic (useful) information about system operation - INFO - - # A warning - WARN - - # A handleable error condition - ERROR - - # An unhandleable error that results in a program crash - FATAL - - UNKNOWN - end - - alias Formatter = Severity, Time, String, String, IO -> - - private DEFAULT_FORMATTER = Formatter.new do |severity, datetime, progname, message, io| - label = severity.unknown? ? "ANY" : severity.to_s - io << label[0] << ", [" << datetime << " #" << Process.pid << "] " - io << label.rjust(5) << " -- " << progname << ": " << message - end - - # :nodoc: - record Message, - severity : Severity, - datetime : Time, - progname : String, - message : String - - # Creates a new logger that will log to the given *io*. - # If *io* is `nil` then all log calls will be silently ignored. - @[Deprecated("Use `Log` module instead")] - def initialize(@io : IO?, @level = Severity::INFO, @formatter = DEFAULT_FORMATTER, @progname = "") - @closed = false - @mutex = Mutex.new(:unchecked) - end - - # Calls the *close* method on the object passed to `initialize`. - def close - return if @closed - return unless io = @io - @closed = true - - @mutex.synchronize do - io.close - end - end - - {% for name in Severity.constants %} - {{name.id}} = Severity::{{name.id}} - - # Returns `true` if the logger's current severity is lower or equal to `{{name.id}}`. - def {{name.id.downcase}}? - level <= Severity::{{name.id}} - end - - # Logs *message* if the logger's current severity is lower or equal to `{{name.id}}`. - # *progname* overrides a default progname set in this logger. - def {{name.id.downcase}}(message, progname = nil) - log(Severity::{{name.id}}, message, progname) - end - - # Logs the message as returned from the given block if the logger's current severity - # is lower or equal to `{{name.id}}`. The block is not run if the severity is higher. - # *progname* overrides a default progname set in this logger. - def {{name.id.downcase}}(progname = nil) - log(Severity::{{name.id}}, progname) { yield } - end - {% end %} - - # Logs *message* if *severity* is higher or equal with the logger's current - # severity. *progname* overrides a default progname set in this logger. - @[Deprecated("Use `Log` module instead")] - def log(severity, message, progname = nil) - return if severity < level || !@io - write(severity, Time.local, progname || @progname, message) - end - - # Logs the message as returned from the given block if *severity* - # is higher or equal with the loggers current severity. The block is not run - # if *severity* is lower. *progname* overrides a default progname set in this logger. - @[Deprecated("Use `Log` module instead")] - def log(severity, progname = nil) - return if severity < level || !@io - write(severity, Time.local, progname || @progname, yield) - end - - private def write(severity, datetime, progname, message) - io = @io - return unless io - - progname_to_s = progname.to_s - message_to_s = message.to_s - - @mutex.synchronize do - formatter.call(severity, datetime, progname_to_s, message_to_s, io) - io.puts - io.flush - end - end -end diff --git a/src/math/libm.cr b/src/math/libm.cr index e04a83c505b9..0899fc3998ac 100644 --- a/src/math/libm.cr +++ b/src/math/libm.cr @@ -1,4 +1,4 @@ -{% if flag?(:linux) || flag?(:freebsd) || flag?(:openbsd) || flag?(:dragonfly) %} +{% if flag?(:linux) || flag?(:bsd) %} @[Link("m")] {% end %} diff --git a/src/named_tuple.cr b/src/named_tuple.cr index 09b56f44af84..fc5b76d8a732 100644 --- a/src/named_tuple.cr +++ b/src/named_tuple.cr @@ -266,7 +266,13 @@ struct NamedTuple {% end %} end - protected def sorted_keys + # Returns a `Tuple` of symbols with the keys in this named tuple, sorted by name. + # + # ``` + # tuple = {foo: 1, bar: 2, baz: 3} + # tuple.sorted_keys # => {:bar, :baz, :foo} + # ``` + def sorted_keys {% begin %} Tuple.new( {% for key in T.keys.sort %} diff --git a/src/oauth2/auth_scheme.cr b/src/oauth2/auth_scheme.cr new file mode 100644 index 000000000000..1402239d4b15 --- /dev/null +++ b/src/oauth2/auth_scheme.cr @@ -0,0 +1,15 @@ +# Enum of supported mechanisms used to pass credentials to the server. +# +# According to https://tools.ietf.org/html/rfc6749#section-2.3.1: +# +# > "Including the client credentials in the request-body using the +# > two parameters is NOT RECOMMENDED and SHOULD be limited to +# > clients unable to directly utilize the HTTP Basic authentication +# > scheme (or other password-based HTTP authentication schemes)." +# +# Therefore, HTTP Basic is preferred, and Request Body should only +# be used if the server does not support HTTP Basic. +enum OAuth2::AuthScheme + HTTPBasic + RequestBody +end diff --git a/src/oauth2/client.cr b/src/oauth2/client.cr index b6b14e852d49..71e1ab2f498b 100644 --- a/src/oauth2/client.cr +++ b/src/oauth2/client.cr @@ -60,12 +60,18 @@ class OAuth2::Client # *token_uri* can be relative or absolute. # If they are relative, the given *host*, *port* and *scheme* will be used. # If they are absolute, the absolute URL will be used. + # + # As per https://tools.ietf.org/html/rfc6749#section-2.3.1, + # `AuthScheme::HTTPBasic` is the default *auth_scheme* (the mechanism used to + # transmit the client credentials to the server). `AuthScheme::RequestBody` should + # only be used if the server does not support HTTP Basic. def initialize(@host : String, @client_id : String, @client_secret : String, @port : Int32? = nil, @scheme = "https", @authorize_uri = "/oauth2/authorize", @token_uri = "/oauth2/token", - @redirect_uri : String? = nil) + @redirect_uri : String? = nil, + @auth_scheme : AuthScheme = :http_basic) end # Builds an authorize URI, as specified by @@ -145,18 +151,26 @@ class OAuth2::Client end private def get_access_token : AccessToken - body = HTTP::Params.build do |form| - form.add("client_id", @client_id) - form.add("client_secret", @client_secret) - yield form - end - headers = HTTP::Headers{ "Accept" => "application/json", "Content-Type" => "application/x-www-form-urlencoded", } - response = HTTP::Client.post(token_uri, form: body, headers: headers) + body = HTTP::Params.build do |form| + case @auth_scheme + when .request_body? + form.add("client_id", @client_id) + form.add("client_secret", @client_secret) + when .http_basic? + headers.add( + "Authorization", + "Basic #{Base64.strict_encode("#{@client_id}:#{@client_secret}")}" + ) + end + yield form + end + + response = HTTP::Client.post token_uri, form: body, headers: headers case response.status when .ok?, .created? OAuth2::AccessToken.from_json(response.body) @@ -165,15 +179,12 @@ class OAuth2::Client end end - private def token_uri + private def token_uri : URI uri = URI.parse(@token_uri) - if uri.host - # If it's an absolute URI, use that one - @token_uri + uri else - # Otherwise use the default one - URI.new(@scheme, @host, @port, @token_uri).to_s + URI.new(@scheme, @host, @port, @token_uri) end end end diff --git a/src/object.cr b/src/object.cr index 4adfaab51a2c..e6f3a777dd43 100644 --- a/src/object.cr +++ b/src/object.cr @@ -1287,6 +1287,9 @@ class Object # Defines an `==` method by comparing the given fields. # # The generated `==` method has a `self` restriction. + # For classes it will first compare by reference and return `true` + # when an object instance is compared with itself, without comparing + # any of the fields. # # ``` # class Person @@ -1299,6 +1302,9 @@ class Object # ``` macro def_equals(*fields) def ==(other : self) + {% if @type.class? %} + return true if same?(other) + {% end %} {% for field in fields %} return false unless {{field.id}} == other.{{field.id}} {% end %} diff --git a/src/openssl/digest/digest.cr b/src/openssl/digest/digest.cr index c1cb2504c1b5..59b9692c9683 100644 --- a/src/openssl/digest/digest.cr +++ b/src/openssl/digest/digest.cr @@ -1,8 +1,9 @@ require "../lib_crypto" +require "digest/base" require "./digest_base" module OpenSSL - class Digest + class Digest < ::Digest::Base class Error < OpenSSL::Error; end class UnsupportedError < Error; end @@ -38,40 +39,41 @@ module OpenSSL LibCrypto.evp_md_ctx_free(self) end - def clone + def dup ctx = LibCrypto.evp_md_ctx_new if LibCrypto.evp_md_ctx_copy(ctx, @ctx) == 0 LibCrypto.evp_md_ctx_free(ctx) - raise Error.new("Unable to clone digest") + raise Error.new("Unable to dup digest") end Digest.new(@name, ctx) end - def reset + private def reset_impl : Nil if LibCrypto.evp_digestinit_ex(self, to_unsafe_md, nil) != 1 raise Error.new "Digest initialization failed." end - self end - def update(data : String | Slice) - LibCrypto.evp_digestupdate(self, data, data.bytesize) - self + private def update_impl(data : Bytes) : Nil + check_finished + if LibCrypto.evp_digestupdate(self, data, data.bytesize) != 1 + raise Error.new "EVP_DigestUpdate" + end end - protected def finish - size = digest_size - data = Pointer(UInt8).malloc(size) - LibCrypto.evp_digestfinal_ex(@ctx, data, nil) - data.to_slice(size) + private def final_impl(data : Bytes) : Nil + raise ArgumentError.new("data size incorrect") unless data.bytesize == digest_size + if LibCrypto.evp_digestfinal_ex(@ctx, data, nil) != 1 + raise Error.new "EVP_DigestFinal_ex" + end end - def digest_size - LibCrypto.evp_md_size(to_unsafe_md) + def digest_size : Int32 + LibCrypto.evp_md_size(to_unsafe_md).to_i end - def block_size - LibCrypto.evp_md_block_size(to_unsafe_md) + def block_size : Int32 + LibCrypto.evp_md_block_size(to_unsafe_md).to_i end def to_unsafe_md diff --git a/src/openssl/digest/digest_base.cr b/src/openssl/digest/digest_base.cr index 678328e3962d..6a05e31a8973 100644 --- a/src/openssl/digest/digest_base.cr +++ b/src/openssl/digest/digest_base.cr @@ -10,7 +10,7 @@ module OpenSSL end # Reads the io's data and updates the digest with it. - def update(io : IO) : Digest + def update(io : IO) : self buffer = uninitialized UInt8[4096] while (read_bytes = io.read(buffer.to_slice)) > 0 self << buffer.to_slice[0, read_bytes] @@ -19,26 +19,17 @@ module OpenSSL end # :ditto: - def <<(data) : Digest + def <<(data) : self update(data) end - # Clones and finishes the digest. - def digest : Bytes - self.clone.finish - end - # Returns a base64-encoded digest. + @[Deprecated("Use `Base64.strict_encode(final)` instead.")] def base64digest : String Base64.strict_encode(digest) end - # Returns a hexadecimal-encoded digest. - def hexdigest : String - digest.hexstring - end - - # :ditto: + @[Deprecated("Use `io << final.hexstring` instead.")] def to_s(io : IO) : Nil io << hexdigest end diff --git a/src/openssl/digest/digest_io.cr b/src/openssl/digest/digest_io.cr index 3be9a7c049c9..4093725fefcc 100644 --- a/src/openssl/digest/digest_io.cr +++ b/src/openssl/digest/digest_io.cr @@ -12,7 +12,7 @@ module OpenSSL # io = OpenSSL::DigestIO.new(underlying_io, "SHA256") # buffer = Bytes.new(256) # io.read(buffer) - # io.digest.hexstring # => "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" + # io.final.hexstring # => "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae" # ``` class DigestIO < IO getter io : IO diff --git a/src/openssl/lib_ssl.cr b/src/openssl/lib_ssl.cr index 455e83c41ab5..688264c488bb 100644 --- a/src/openssl/lib_ssl.cr +++ b/src/openssl/lib_ssl.cr @@ -96,8 +96,12 @@ lib LibSSL NO_SSL_V3 = 0x02000000 NO_TLS_V1 = 0x04000000 + NO_TLS_V1_3 = 0x20000000 NO_TLS_V1_2 = 0x08000000 NO_TLS_V1_1 = 0x10000000 + {% if compare_versions(OPENSSL_VERSION, "1.1.0") >= 0 %} + NO_RENEGOTIATION = 0x40000000 + {% end %} NETSCAPE_CA_DN_BUG = 0x20000000 NETSCAPE_DEMO_CIPHER_CHANGE_BUG = 0x40000000 diff --git a/src/openssl/ssl/context.cr b/src/openssl/ssl/context.cr index bfe3707f7fcf..00f58393d8db 100644 --- a/src/openssl/ssl/context.cr +++ b/src/openssl/ssl/context.cr @@ -5,8 +5,6 @@ require "uri/punycode" # For both server and client applications exist more specialized subclassses # `SSL::Context::Server` and `SSL::Context::Client` which need to be instantiated # appropriately. -# -# All instances use `CIPHERS_INTERMEDIATE` ciphers by default. abstract class OpenSSL::SSL::Context # :nodoc: def self.default_method @@ -18,6 +16,8 @@ abstract class OpenSSL::SSL::Context end class Client < Context + @hostname : String? + # Generates a new TLS client context with sane defaults for a client connection. # # Defaults to `TLS_method` or `SSLv23_method` (depending on OpenSSL version) @@ -36,9 +36,8 @@ abstract class OpenSSL::SSL::Context # context = OpenSSL::SSL::Context::Client.new # context.add_options(OpenSSL::SSL::Options::NO_SSL_V2 | OpenSSL::SSL::Options::NO_SSL_V3) # ``` - - @hostname : String? - + # + # It uses `CIPHERS_OLD` compatibility level by default. def initialize(method : LibSSL::SSLMethod = Context.default_method) super(method) @@ -46,6 +45,8 @@ abstract class OpenSSL::SSL::Context {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.0.2") >= 0 %} self.default_verify_param = "ssl_server" {% end %} + + self.ciphers = CIPHERS_OLD end # Returns a new TLS client context with only the given method set. @@ -115,6 +116,8 @@ abstract class OpenSSL::SSL::Context # context = OpenSSL::SSL::Context::Server.new # context.add_options(OpenSSL::SSL::Options::NO_SSL_V2 | OpenSSL::SSL::Options::NO_SSL_V3) # ``` + # + # It uses `CIPHERS_INTERMEDIATE` compatibility level by default. def initialize(method : LibSSL::SSLMethod = Context.default_method) super(method) @@ -124,6 +127,8 @@ abstract class OpenSSL::SSL::Context {% end %} set_tmp_ecdh_key(curve: LibCrypto::NID_X9_62_prime256v1) + + self.ciphers = CIPHERS_INTERMEDIATE end # Returns a new TLS server context with only the given method set. @@ -186,9 +191,11 @@ abstract class OpenSSL::SSL::Context SINGLE_DH_USE )) - add_modes(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS)) + {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.1.0") >= 0 %} + add_options(OpenSSL::SSL::Options::NO_RENEGOTIATION) + {% end %} - self.ciphers = CIPHERS_INTERMEDIATE + add_modes(OpenSSL::SSL::Modes.flags(AUTO_RETRY, RELEASE_BUFFERS)) end # Overriding initialize or new in the child classes as public methods, @@ -212,7 +219,7 @@ abstract class OpenSSL::SSL::Context LibSSL.ssl_ctx_free(@handle) end - # Sets the default paths for `ca_certiifcates=` and `ca_certificates_path=`. + # Sets the default paths for `ca_certificates=` and `ca_certificates_path=`. def set_default_verify_paths LibSSL.ssl_ctx_set_default_verify_paths(@handle) end @@ -407,7 +414,7 @@ abstract class OpenSSL::SSL::Context when "peer" context.verify_mode = OpenSSL::SSL::VerifyMode::PEER when "force-peer" - context.verify_mode = OpenSSL::SSL::VerifyMode::FAIL_IF_NO_PEER_CERT + context.verify_mode = OpenSSL::SSL::VerifyMode::PEER | OpenSSL::SSL::VerifyMode::FAIL_IF_NO_PEER_CERT when "none" context.verify_mode = OpenSSL::SSL::VerifyMode::NONE when nil diff --git a/src/openssl/ssl/socket.cr b/src/openssl/ssl/socket.cr index 2869d7988c30..cbab93538402 100644 --- a/src/openssl/ssl/socket.cr +++ b/src/openssl/ssl/socket.cr @@ -144,7 +144,6 @@ abstract class OpenSSL::SSL::Socket < IO unless bytes > 0 raise OpenSSL::SSL::Error.new(@ssl, bytes, "SSL_write") end - nil end def unbuffered_flush diff --git a/src/option_parser.cr b/src/option_parser.cr index f0a4f1cecc83..c9f500f2c955 100644 --- a/src/option_parser.cr +++ b/src/option_parser.cr @@ -58,7 +58,7 @@ # welcome = true # parser.banner = "Usage: example welcome" # end -# parser.on("-v", "--verbose", "Enabled servose output") { verbose = true } +# parser.on("-v", "--verbose", "Enabled verbose output") { verbose = true } # parser.on("-h", "--help", "Show this help") do # puts parser # exit @@ -203,16 +203,16 @@ class OptionParser private def parse_flag_definition(flag : String) case flag - when /--(\S+)\s+\[\S+\]/ + when /\A--(\S+)\s+\[\S+\]\z/ {"--#{$1}", FlagValue::Optional} - when /--(\S+)(\s+|\=)(\S+)?/ + when /\A--(\S+)(\s+|\=)(\S+)?\z/ {"--#{$1}", FlagValue::Required} - when /--\S+/ + when /\A--\S+\z/ # This can't be merged with `else` otherwise /-(.)/ matches {flag, FlagValue::None} - when /-(.)\s*\[\S+\]/ + when /\A-(.)\s*\[\S+\]\z/ {flag[0..1], FlagValue::Optional} - when /-(.)\s+\S+/, /-(.)\s+/, /-(.)\S+/ + when /\A-(.)\s+\S+\z/, /\A-(.)\s+\z/, /\A-(.)\S+\z/ {flag[0..1], FlagValue::Required} else # This happens for -f without argument @@ -221,7 +221,7 @@ class OptionParser end # Adds a separator, with an optional header message, that will be used to - # print the help. The seperator is placed between the flags registered (`#on`) + # print the help. The separator is placed between the flags registered (`#on`) # before, and the flags registered after the call. # # This way, you can group the different options in an easier to read way. @@ -274,7 +274,7 @@ class OptionParser io << banner io << '\n' end - @flags.join '\n', io + @flags.join io, '\n' end private def append_flag(flag, description) @@ -366,7 +366,9 @@ class OptionParser value = nil end - if handler = @handlers[flag]? + # Fetch handler of the flag. + # If value is given even though handler does not take value, it is invalid, then it is skipped. + if (handler = @handlers[flag]?) && !(handler.value_type.none? && value) handled_args << arg_index # Pull in the next argument if we don't already have it and an argument diff --git a/src/path.cr b/src/path.cr index 9f4afcc6cf2d..f17878cc80f5 100644 --- a/src/path.cr +++ b/src/path.cr @@ -9,8 +9,8 @@ # A `Path` can represent a root, a root and a sequence of names, or simply one or # more name elements. # A `Path` is considered to be an empty path if it consists solely of one name -# element that is empty. Accessing a file using an empty path is equivalent -# to accessing the default directory of the process. +# element that is empty or equal to `"."`. Accessing a file using an empty path +# is equivalent to accessing the default directory of the process. # # # Examples # @@ -230,7 +230,7 @@ struct Path # Returns the parent path of this path. # - # If the path is empty or `"."`, it returns `"."`. If the path is rooted + # If the path is empty, it returns `"."`. If the path is rooted # and in the top-most hierarchy, the root path is returned. # # ``` @@ -267,7 +267,7 @@ struct Path # # Path["foo/bar"] # ``` def each_parent(&block : Path ->) - return if @name.empty? || @name == "." + return if empty? first_char = @name.char_at(0) unless separators.includes?(first_char) || (first_char == '.' && separators.includes?(@name.byte_at?(1).try &.unsafe_chr)) || (windows? && (windows_drive? || unc_share?)) @@ -398,7 +398,7 @@ struct Path # # See also Rob Pike: *[Lexical File Names in Plan 9 or Getting Dot-Dot Right](https://9p.io/sys/doc/lexnames.html)* def normalize(*, remove_final_separator : Bool = true) : Path - return new_instance "." if @name.empty? + return new_instance "." if empty? drive, root = drive_and_root reader = Char::Reader.new(@name) @@ -828,7 +828,7 @@ struct Path # Copy the part buffer.copy_from(part_ptr, part_bytesize) - {bytesize, @name.ascii_only? && part.ascii_only? ? bytesize : 0} + {bytesize, @name.single_byte_optimizable? && part.single_byte_optimizable? ? bytesize : 0} end new_instance new_name @@ -946,6 +946,10 @@ struct Path end end + private def empty? + @name.empty? || @name == "." + end + # Returns a relative path that is lexically equivalent to `self` when joined # to *base* with an intervening separator. # diff --git a/src/pointer.cr b/src/pointer.cr index 87580a2c865a..b808da0ffda8 100644 --- a/src/pointer.cr +++ b/src/pointer.cr @@ -326,7 +326,7 @@ struct Pointer(T) io << ".null" else io << "@0x" - address.to_s(16, io) + address.to_s(io, 16) end end diff --git a/src/prelude.cr b/src/prelude.cr index 810e27b395fc..5ccd0f959358 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -7,12 +7,6 @@ # to also add them to `docs_main.cr` if their content needs to # appear in the API docs. -private macro no_win(stmt) - {% unless flag?(:win32) %} - {{stmt}} - {% end %} -end - # This list requires ordered statements require "crystal/once" require "lib_c" @@ -57,7 +51,9 @@ require "intrinsics" require "io" require "kernel" require "math/math" -no_win require "mutex" +{% unless flag?(:win32) %} + require "mutex" +{% end %} require "named_tuple" require "nil" require "humanize" @@ -73,7 +69,9 @@ require "range" require "reference" require "regex" require "set" -no_win require "signal" +{% unless flag?(:win32) %} + require "signal" +{% end %} require "slice" require "static_array" require "struct" diff --git a/src/pretty_print.cr b/src/pretty_print.cr index 0b32809504a5..7dbf4be6a7cf 100644 --- a/src/pretty_print.cr +++ b/src/pretty_print.cr @@ -191,6 +191,8 @@ class PrettyPrint end @buffer.clear @buffer_width = 0 + + @output.flush end private class Text diff --git a/src/proc.cr b/src/proc.cr index 4240842b9da3..b73062f3a5b1 100644 --- a/src/proc.cr +++ b/src/proc.cr @@ -194,7 +194,7 @@ struct Proc io << "#<" io << {{@type.name.stringify}} io << ":0x" - pointer.address.to_s(16, io) + pointer.address.to_s(io, 16) if closure? io << ":closure" end diff --git a/src/process.cr b/src/process.cr index e1088aea6208..3e7eb7bce939 100644 --- a/src/process.cr +++ b/src/process.cr @@ -156,7 +156,7 @@ class Process # # Available only on Unix-like operating systems. def self.exec(command : String, args = nil, env : Env = nil, clear_env : Bool = false, shell : Bool = false, - input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : String? = nil) + input : ExecStdio = Redirect::Inherit, output : ExecStdio = Redirect::Inherit, error : ExecStdio = Redirect::Inherit, chdir : String? = nil) : NoReturn command_args = Crystal::System::Process.prepare_args(command, args, shell) input = exec_stdio_to_fd(input, for: STDIN) diff --git a/src/process/executable_path.cr b/src/process/executable_path.cr index d713d5fe9620..5485a921bb73 100644 --- a/src/process/executable_path.cr +++ b/src/process/executable_path.cr @@ -28,13 +28,47 @@ class Process end end + private def self.is_executable_file?(path) + unless File.info?(path, follow_symlinks: true).try &.file? + return false + end + {% if flag?(:win32) %} + # This is *not* a temporary stub. + # Windows doesn't have "executable" metadata for files, so it also doesn't have files that are "not executable". + true + {% else %} + File.executable?(path) + {% end %} + end + # Searches an executable, checking for an absolute path, a path relative to # *pwd* or absolute path, then eventually searching in directories declared # in *path*. def self.find_executable(name : Path | String, path : String? = ENV["PATH"]?, pwd : Path | String = Dir.current) : String? - name = Path.new(name) + find_executable_possibilities(Path.new(name), path, pwd) do |p| + if is_executable_file?(p) + return p.to_s + end + end + nil + end + + private def self.find_executable_possibilities(name, path, pwd) + return if name.to_s.empty? + + {% if flag?(:win32) %} + # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw#parameters + # > If the file name does not contain an extension, .exe is appended. + # See find_executable_spec.cr for cases this needs to match, based on CreateProcessW behavior. + basename = name.ends_with_separator? ? "" : name.basename + basename = "" if basename == name.anchor.to_s + if (basename.empty? ? !name.anchor : !basename.includes?(".")) + name = Path.new("#{name}.exe") + end + {% end %} + if name.absolute? - return name.to_s + yield name end # check if the name includes a separator @@ -43,19 +77,17 @@ class Process count_parts += 1 break if count_parts > 1 end + has_separator = (count_parts > 1) - if count_parts > 1 - return name.expand(pwd).to_s + if {{ flag?(:win32) }} || has_separator + yield name.expand(pwd) end - return unless path - - path.split(PATH_DELIMITER).each do |path_entry| - executable = Path.new(path_entry, name) - return executable.to_s if File.exists?(executable) + if path && !has_separator + path.split(PATH_DELIMITER).each do |path_entry| + yield Path.new(path_entry, name) + end end - - nil end end @@ -87,6 +119,20 @@ end buf = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*) size = LibC::SizeT.new(LibC::PATH_MAX) + if LibC.sysctl(mib, 4, buf, pointerof(size), nil, 0) == 0 + String.new(buf, size - 1) + end + end + end +{% elsif flag?(:netbsd) %} + require "c/sysctl" + + class Process + private def self.executable_path_impl + mib = Int32[LibC::CTL_KERN, LibC::KERN_PROC_ARGS, -1, LibC::KERN_PROC_PATHNAME] + buf = GC.malloc_atomic(LibC::PATH_MAX).as(UInt8*) + size = LibC::SizeT.new(LibC::PATH_MAX) + if LibC.sysctl(mib, 4, buf, pointerof(size), nil, 0) == 0 String.new(buf, size - 1) end diff --git a/src/process/shell.cr b/src/process/shell.cr new file mode 100644 index 000000000000..a356302fe430 --- /dev/null +++ b/src/process/shell.cr @@ -0,0 +1,161 @@ +class Process + # Converts a sequence of strings to one joined string with each argument shell-quoted. + # + # This is then safe to pass as part of the command when using `shell: true` or `system()`. + # + # NOTE: The actual return value is system-dependent, so it mustn't be relied on in other contexts. + # See also: `quote_posix`. + # + # ``` + # files = ["my file.txt", "another.txt"] + # `grep -E 'fo+' -- #{Process.quote(files)}` + # ``` + def self.quote(args : Enumerable(String)) : String + {% if flag?(:win32) %} + quote_windows(args) + {% else %} + quote_posix(args) + {% end %} + end + + # Shell-quotes one item, same as `quote({arg})`. + def self.quote(arg : String) : String + quote({arg}) + end + + # Converts a sequence of strings to one joined string with each argument shell-quoted. + # + # This is then safe to pass to a POSIX shell. + # + # ``` + # files = ["my file.txt", "another.txt"] + # Process.quote_posix(files) # => "'my file.txt' another.txt" + # ``` + def self.quote_posix(args : Enumerable(String)) : String + args.join(' ') do |arg| + if arg.empty? + "''" + elsif arg.matches? %r([^a-zA-Z0-9%+,\-./:=@_]) # not all characters are safe, needs quoting + "'" + arg.gsub("'", %('"'"')) + "'" # %(foo'ba#r) becomes %('foo'"'"'ba#r') + else + arg + end + end + end + + # Shell-quotes one item, same as `quote_posix({arg})`. + def self.quote_posix(arg : String) : String + quote_posix({arg}) + end + + # :nodoc: + # + # Converts a sequence of strings to one joined string with each argument shell-quoted. + # + # This is then safe to pass Windows API CreateProcess. + # + # NOTE: This is **not** safe to pass to the CMD shell. + # + # ``` + # files = ["my file.txt", "another.txt"] + # Process.quote_windows(files) # => %("my file.txt" another.txt) + # ``` + def self.quote_windows(args : Enumerable(String)) : String + String.build { |io| quote_windows(io, args) } + end + + private def self.quote_windows(io : IO, args) + args.join(io, ' ') do |arg| + need_quotes = arg.empty? || arg.includes?(' ') || arg.includes?('\t') + + io << '"' if need_quotes + + slashes = 0 + arg.each_char do |c| + case c + when '\\' + slashes += 1 + when '"' + (slashes + 1).times { io << '\\' } + slashes = 0 + else + slashes = 0 + end + + io << c + end + + if need_quotes + slashes.times { io << '\\' } + io << '"' + end + end + end + + # :nodoc: + # + # Shell-quotes one item, same as `quote_windows({arg})`. + # + # ``` + # Process.quote_windows(%q(C:\"foo" project.txt)) # => %q("C:\\\"foo\" project.txt") + # ``` + def self.quote_windows(arg : String) : String + quote_windows({arg}) + end + + # Split a *line* string into the array of tokens in the same way the POSIX shell. + # + # ``` + # Process.parse_arguments(%q["foo bar" '\hello/' Fizz\ Buzz]) # => ["foo bar", "\\hello/", "Fizz Buzz"] + # ``` + # + # See https://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_03 + def self.parse_arguments(line : String) : Array(String) + tokens = [] of String + + reader = Char::Reader.new(line) + + while reader.has_next? + # skip whitespace + while reader.current_char.ascii_whitespace? + reader.next_char + end + break unless reader.has_next? + + token = String.build do |str| + while reader.has_next? && !reader.current_char.ascii_whitespace? + quote = nil + if reader.current_char.in?('\'', '"') + quote = reader.current_char + reader.next_char + end + + until (char = reader.current_char) == quote || (!quote && char.ascii_whitespace?) + break unless reader.has_next? + reader.next_char + if char == '\\' && quote != '\'' + str << char if quote == '"' + char = reader.current_char + if reader.has_next? + reader.next_char + else + break if quote == '"' + char = '\\' + end + end + str << char + end + + if quote + raise ArgumentError.new("Unmatched quote") unless reader.has_next? + reader.next_char + end + end + end + + tokens << token + end + + tokens + end +end diff --git a/src/raise.cr b/src/raise.cr index 638b81553c88..126923c534ad 100644 --- a/src/raise.cr +++ b/src/raise.cr @@ -195,6 +195,7 @@ end ret = LibUnwind.raise_exception(unwind_ex) Crystal::System.print_error "Failed to raise an exception: %s\n", ret.to_s Exception::CallStack.print_backtrace + Crystal::System.print_exception("\nTried to raise:", unwind_ex.value.exception_object.as(Exception)) LibC.exit(ret) end diff --git a/src/range.cr b/src/range.cr index 5f39162dfdef..b4497b17ae92 100644 --- a/src/range.cr +++ b/src/range.cr @@ -112,12 +112,22 @@ struct Range(B, E) raise ArgumentError.new("Can't each beginless range") end - end_value = @end - while end_value.nil? || current < end_value - yield current - current = current.succ - end - yield current if !@exclusive && current == end_value + # TODO: This typeof and the macro interpolations are a workaround until #9324 is fixed. + typeof(yield current) + + {% if E == Nil %} + while true + {{ "yield current".id }} + current = current.succ + end + {% else %} + end_value = @end + while end_value.nil? || current < end_value + {{ "yield current".id }} + current = current.succ + end + {{ "yield current".id }} if !@exclusive && current == end_value + {% end %} end # Returns an `Iterator` over the elements of this range. @@ -159,10 +169,19 @@ struct Range(B, E) yield end_value if !@exclusive && (begin_value.nil? || !(end_value < begin_value)) current = end_value - while begin_value.nil? || begin_value < current - current = current.pred - yield current - end + # TODO: The macro interpolations are a workaround until #9324 is fixed. + + {% if B == Nil %} + while true + current = current.pred + {{ "yield current".id }} + end + {% else %} + while begin_value.nil? || begin_value < current + current = current.pred + {{ "yield current".id }} + end + {% end %} end # Returns a reverse `Iterator` over the elements of this range. diff --git a/src/reference.cr b/src/reference.cr index d6324c2ff550..8a64357fc112 100644 --- a/src/reference.cr +++ b/src/reference.cr @@ -70,7 +70,7 @@ class Reference # ``` def inspect(io : IO) : Nil io << "#<" << {{@type.name.id.stringify}} << ":0x" - object_id.to_s(16, io) + object_id.to_s(io, 16) executed = exec_recursive(:inspect) do {% for ivar, i in @type.instance_vars %} @@ -129,7 +129,7 @@ class Reference # ``` def to_s(io : IO) : Nil io << "#<" << self.class.name << ":0x" - object_id.to_s(16, io) + object_id.to_s(io, 16) io << '>' end diff --git a/src/semantic_version.cr b/src/semantic_version.cr index 82c2197dbd2c..8828a7d8447c 100644 --- a/src/semantic_version.cr +++ b/src/semantic_version.cr @@ -147,7 +147,7 @@ struct SemanticVersion # semver.prerelease.to_s # => "rc.1" # ``` def to_s(io : IO) : Nil - identifiers.join('.', io) + identifiers.join(io, '.') end # The comparison operator. diff --git a/src/set.cr b/src/set.cr index f17d4cca390d..581df71eed82 100644 --- a/src/set.cr +++ b/src/set.cr @@ -404,7 +404,7 @@ struct Set(T) # Writes a string representation of the set to *io*. def to_s(io : IO) : Nil io << "Set{" - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << '}' end diff --git a/src/slice.cr b/src/slice.cr index 0c011d31a6a7..6ca401b85d77 100644 --- a/src/slice.cr +++ b/src/slice.cr @@ -357,7 +357,7 @@ struct Slice(T) # Copies the contents of this slice into *target*. # - # Raises `IndexError` if the desination slice cannot fit the data being transferred + # Raises `IndexError` if the destination slice cannot fit the data being transferred # e.g. dest.size < self.size. # # ``` @@ -376,7 +376,7 @@ struct Slice(T) # Copies the contents of *source* into this slice. # - # Raises `IndexError` if the desination slice cannot fit the data being transferred. + # Raises `IndexError` if the destination slice cannot fit the data being transferred. @[AlwaysInline] def copy_from(source : self) source.copy_to(self) @@ -396,7 +396,7 @@ struct Slice(T) # Moves the contents of this slice into *target*. *target* and `self` may # overlap; the copy is always done in a non-destructive manner. # - # Raises `IndexError` if the desination slice cannot fit the data being transferred + # Raises `IndexError` if the destination slice cannot fit the data being transferred # e.g. `dest.size < self.size`. # # ``` @@ -418,7 +418,7 @@ struct Slice(T) # Moves the contents of *source* into this slice. *source* and `self` may # overlap; the copy is always done in a non-destructive manner. # - # Raises `IndexError` if the desination slice cannot fit the data being transferred. + # Raises `IndexError` if the destination slice cannot fit the data being transferred. @[AlwaysInline] def move_from(source : self) source.move_to(self) @@ -596,11 +596,11 @@ struct Slice(T) if T == UInt8 io << "Bytes[" # Inspect using to_s because we know this is a UInt8. - join ", ", io, &.to_s(io) + join io, ", ", &.to_s(io) io << ']' else io << "Slice[" - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << ']' end end diff --git a/src/socket.cr b/src/socket.cr index 634939432ba6..5e13b00f0dee 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -155,7 +155,7 @@ class Socket < IO when Errno::EISCONN return when Errno::EINPROGRESS, Errno::EALREADY - wait_writable(timeout: timeout) do |error| + wait_writable(timeout: timeout) do return yield IO::TimeoutError.new("connect timed out") end else @@ -271,7 +271,8 @@ class Socket < IO if closed? return elsif Errno.value == Errno::EAGAIN - wait_readable rescue nil + wait_acceptable + return if closed? else raise Socket::Error.from_errno("accept") end @@ -281,6 +282,12 @@ class Socket < IO end end + private def wait_acceptable + wait_readable(raise_if_closed: false) do + raise TimeoutError.new("Accept timed out") + end + end + # Sends a message to a previously connected remote address. # # ``` diff --git a/src/socket/address.cr b/src/socket/address.cr index 550605e1ab5a..6dba99637fee 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -50,10 +50,6 @@ class Socket end abstract def to_unsafe : LibC::Sockaddr* - - def ==(other) - false - end end # IP address representation. @@ -71,7 +67,7 @@ class Socket # ``` # # `IPAddress` won't resolve domains, including `localhost`. If you must - # resolve an IP, or don't know whether a `String` constains an IP or a domain + # resolve an IP, or don't know whether a `String` contains an IP or a domain # name, you should use `Addrinfo.resolve` instead. struct IPAddress < Address UNSPECIFIED = "0.0.0.0" @@ -83,15 +79,15 @@ class Socket getter port : Int32 - @address : String? - @addr6 : LibC::In6Addr? - @addr4 : LibC::InAddr? + @addr : LibC::In6Addr | LibC::InAddr def initialize(@address : String, @port : Int32) - if @addr6 = ip6?(address) + if addr = ip6?(address) + @addr = addr @family = Family::INET6 @size = sizeof(LibC::SockaddrIn6) - elsif @addr4 = ip4?(address) + elsif addr = ip4?(address) + @addr = addr @family = Family::INET @size = sizeof(LibC::SockaddrIn) else @@ -148,7 +144,7 @@ class Socket protected def initialize(sockaddr : LibC::SockaddrIn6*, @size) @family = Family::INET6 - @addr6 = sockaddr.value.sin6_addr + @addr = sockaddr.value.sin6_addr @port = {% if flag?(:dragonfly) %} Intrinsics.bswap16(sockaddr.value.sin6_port).to_i @@ -159,7 +155,7 @@ class Socket protected def initialize(sockaddr : LibC::SockaddrIn*, @size) @family = Family::INET - @addr4 = sockaddr.value.sin_addr + @addr = sockaddr.value.sin_addr @port = {% if flag?(:dragonfly) %} Intrinsics.bswap16(sockaddr.value.sin_port).to_i @@ -185,15 +181,7 @@ class Socket # ip_address = socket.remote_address # ip_address.address # => "127.0.0.1" # ``` - def address - @address ||= begin - case family - when Family::INET6 then address(@addr6.not_nil!) - when Family::INET then address(@addr4.not_nil!) - else raise "Unsupported IP address family: #{family}" - end - end - end + getter(address : String) { address(@addr) } private def address(addr : LibC::In6Addr) String.new(46) do |buffer| @@ -218,28 +206,26 @@ class Socket # In the IPv4 family, loopback addresses are all addresses in the subnet # `127.0.0.0/24`. In IPv6 `::1` is the loopback address. def loopback? : Bool - if addr = @addr4 + case addr = @addr + in LibC::InAddr addr.s_addr & 0x00000000ff_u32 == 0x0000007f_u32 - elsif addr = @addr6 + in LibC::In6Addr ipv6_addr8(addr) == StaticArray[0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 1_u8] - else - raise "unreachable!" end end # Returns `true` if this IP is an unspecified address, either the IPv4 address `0.0.0.0` or the IPv6 address `::`. def unspecified? : Bool - if addr = @addr4 + case addr = @addr + in LibC::InAddr addr.s_addr == 0_u32 - elsif addr = @addr6 + in LibC::In6Addr ipv6_addr8(addr) == StaticArray[0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8] - else - raise "unreachable!" end end private def ipv6_addr8(addr : LibC::In6Addr) - {% if flag?(:darwin) || flag?(:openbsd) || flag?(:freebsd) || flag?(:dragonfly) %} + {% if flag?(:darwin) || flag?(:bsd) %} addr.__u6_addr.__u6_addr8 {% elsif flag?(:linux) && flag?(:musl) %} addr.__in6_union.__s6_addr @@ -275,17 +261,15 @@ class Socket end def to_unsafe : LibC::Sockaddr* - case family - when Family::INET6 - to_sockaddr_in6 - when Family::INET - to_sockaddr_in - else - raise "Unsupported IP address family: #{family}" + case addr = @addr + in LibC::InAddr + to_sockaddr_in(addr) + in LibC::In6Addr + to_sockaddr_in6(addr) end end - private def to_sockaddr_in6 + private def to_sockaddr_in6(addr) sockaddr = Pointer(LibC::SockaddrIn6).malloc sockaddr.value.sin6_family = family {% if flag?(:dragonfly) %} @@ -293,11 +277,11 @@ class Socket {% else %} sockaddr.value.sin6_port = LibC.htons(port) {% end %} - sockaddr.value.sin6_addr = @addr6.not_nil! + sockaddr.value.sin6_addr = addr sockaddr.as(LibC::Sockaddr*) end - private def to_sockaddr_in + private def to_sockaddr_in(addr) sockaddr = Pointer(LibC::SockaddrIn).malloc sockaddr.value.sin_family = family {% if flag?(:dragonfly) %} @@ -305,7 +289,7 @@ class Socket {% else %} sockaddr.value.sin_port = LibC.htons(port) {% end %} - sockaddr.value.sin_addr = @addr4.not_nil! + sockaddr.value.sin_addr = addr sockaddr.as(LibC::Sockaddr*) end end diff --git a/src/socket/tcp_socket.cr b/src/socket/tcp_socket.cr index 7a5bdbcd2d0f..cbf105c80b7b 100644 --- a/src/socket/tcp_socket.cr +++ b/src/socket/tcp_socket.cr @@ -60,7 +60,7 @@ class TCPSocket < IPSocket end end - # Returns `true` if the Nable algorithm is disabled. + # Returns `true` if the Nagle algorithm is disabled. def tcp_nodelay? getsockopt_bool LibC::TCP_NODELAY, level: Protocol::TCP end @@ -75,6 +75,8 @@ class TCPSocket < IPSocket def tcp_keepalive_idle optname = {% if flag?(:darwin) %} LibC::TCP_KEEPALIVE + {% elsif flag?(:netbsd) %} + LibC::SO_KEEPALIVE {% else %} LibC::TCP_KEEPIDLE {% end %} @@ -84,6 +86,8 @@ class TCPSocket < IPSocket def tcp_keepalive_idle=(val : Int) optname = {% if flag?(:darwin) %} LibC::TCP_KEEPALIVE + {% elsif flag?(:netbsd) %} + LibC::SO_KEEPALIVE {% else %} LibC::TCP_KEEPIDLE {% end %} diff --git a/src/socket/udp_socket.cr b/src/socket/udp_socket.cr index 6887d5cc9706..5f25c6b1ce30 100644 --- a/src/socket/udp_socket.cr +++ b/src/socket/udp_socket.cr @@ -160,7 +160,8 @@ class UDPSocket < IPSocket # Raises `Socket::Error` unless the socket is IPv4 and an IPv4 address is provided. def multicast_interface(address : IPAddress) if @family == Family::INET - if addr = address.@addr4 + addr = address.@addr + if addr.is_a?(LibC::InAddr) setsockopt LibC::IP_MULTICAST_IF, addr, LibC::IPPROTO_IP else raise Socket::Error.new "Expecting an IPv4 interface address. Address provided: #{address.address}" @@ -211,9 +212,11 @@ class UDPSocket < IPSocket end private def group_modify(ip, operation) + ip_addr = ip.@addr + case @family when Family::INET - if ip_addr = ip.@addr4 + if ip_addr.is_a?(LibC::InAddr) req = LibC::IpMreq.new req.imr_multiaddr = ip_addr @@ -222,7 +225,7 @@ class UDPSocket < IPSocket raise Socket::Error.new "Expecting an IPv4 multicast address. Address provided: #{ip.address}" end when Family::INET6 - if ip_addr = ip.@addr6 + if ip_addr.is_a?(LibC::In6Addr) req = LibC::Ipv6Mreq.new req.ipv6mr_multiaddr = ip_addr diff --git a/src/spec.cr b/src/spec.cr index 15d0f2121a3b..c8290f673d0a 100644 --- a/src/spec.cr +++ b/src/spec.cr @@ -28,7 +28,6 @@ require "./spec/dsl" # end # # # lots more specs -# # end # ``` # diff --git a/src/spec/context.cr b/src/spec/context.cr index 18ab10b20d8e..8ba4635d5349 100644 --- a/src/spec/context.cr +++ b/src/spec/context.cr @@ -12,6 +12,100 @@ module Spec end children.shuffle!(randomizer) end + + protected def internal_run + run_before_all_hooks + children.each &.run + run_after_all_hooks + end + + protected def before_each(&block) + (@before_each ||= [] of ->) << block + end + + protected def run_before_each_hooks + @before_each.try &.each &.call + end + + protected def after_each(&block) + (@after_each ||= [] of ->) << block + end + + protected def run_after_each_hooks + @after_each.try &.reverse_each &.call + end + + protected def before_all(&block) + (@before_all ||= [] of ->) << block + end + + protected def run_before_all_hooks + @before_all.try &.each &.call + end + + protected def after_all(&block) + (@after_all ||= [] of ->) << block + end + + protected def run_after_all_hooks + @after_all.try &.reverse_each &.call + end + + protected def around_each(&block : Example::Procsy ->) + (@around_each ||= [] of Example::Procsy ->) << block + end + + protected def run_around_each_hooks(procsy : Example::Procsy) : Bool + internal_run_around_each_hooks(procsy) + end + + protected def internal_run_around_each_hooks(procsy : Example::Procsy) : Bool + around_each = @around_each + return false unless around_each + + run_around_each_hook(around_each, procsy, 0) + true + end + + protected def run_around_each_hook(around_each, procsy, index) : Nil + around_each[index].call( + if index == around_each.size - 1 + # If we don't have any more hooks after this one, call the procsy + procsy + else + # Otherwise, create a procsy that will invoke the next hook + Example::Procsy.new(procsy.example) do + run_around_each_hook(around_each, procsy, index + 1) + end + end + ) + end + + protected def around_all(&block : ExampleGroup::Procsy ->) + (@around_all ||= [] of ExampleGroup::Procsy ->) << block + end + + protected def run_around_all_hooks(procsy : ExampleGroup::Procsy) : Bool + around_all = @around_all + return false unless around_all + + run_around_all_hook(around_all, procsy, 0) + true + end + + protected def run_around_all_hook(around_all, procsy, index) : Nil + around_all[index].call( + if index == around_all.size - 1 + # If we don't have any more hooks after this one, call the procsy + procsy + else + # Otherwise, create a procsy that will invoke the next hook + ExampleGroup::Procsy.new(procsy.example_group) do + run_around_all_hook(around_all, procsy, index + 1) + end + end + ) + end end # :nodoc: @@ -28,12 +122,17 @@ module Spec RootContext.instance end + # :nodoc: + def self.current_context : Context + RootContext.current_context + end + # :nodoc: # # The root context is the main interface that the spec DSL interacts with. class RootContext < Context class_getter instance = RootContext.new - @@current_context : Context = @@instance + class_getter current_context : Context = @@instance def initialize @results = { @@ -45,7 +144,7 @@ module Spec end def run - children.each &.run + internal_run end def report(kind, full_description, file, line, elapsed = nil, ex = nil) @@ -206,68 +305,8 @@ module Spec end end - def before_each(&block) - if @@current_context == self - raise "Can't call `before_each` outside of a describe/context" - end - - @@current_context.before_each(&block) - end - - def run_before_each_hooks - # Nothing - end - - def after_each(&block) - if @@current_context == self - raise "Can't call `after_each` outside of a describe/context" - end - - @@current_context.after_each(&block) - end - - def run_after_each_hooks - # Nothing - end - - def before_all(&block) - if @@current_context == self - raise "Can't call `before_all` outside of a describe/context" - end - - @@current_context.before_all(&block) - end - - def after_all(&block) - if @@current_context == self - raise "Can't call `after_all` outside of a describe/context" - end - - @@current_context.after_all(&block) - end - - def around_each(&block : Example::Procsy ->) - if @@current_context == self - raise "Can't call `around_each` outside of a describe/context" - end - - @@current_context.around_each(&block) - end - - def run_around_each_hooks(procsy : Example::Procsy) : Bool - false - end - - def around_all(&block : ExampleGroup::Procsy ->) - if @@current_context == self - raise "Can't call `around_all` outside of a describe/context" - end - - @@current_context.around_all(&block) - end - - def run_around_all_hooks(procsy : ExampleGroup::Procsy) : Bool - false + protected def around_all(&block : ExampleGroup::Procsy ->) + raise "Can't call `around_all` outside of a describe/context" end end @@ -291,54 +330,20 @@ module Spec Spec.formatters.each(&.pop) end - protected def internal_run - run_before_all_hooks - children.each &.run - run_after_all_hooks - end - protected def report(kind, description, file, line, elapsed = nil, ex = nil) parent.report kind, "#{@description} #{description}", file, line, elapsed, ex end - protected def before_each(&block) - (@before_each ||= [] of ->) << block - end - protected def run_before_each_hooks @parent.run_before_each_hooks - @before_each.try &.each &.call - end - - protected def after_each(&block) - (@after_each ||= [] of ->) << block + super end protected def run_after_each_hooks - @after_each.try &.reverse_each &.call + super @parent.run_after_each_hooks end - protected def before_all(&block) - (@before_all ||= [] of ->) << block - end - - protected def run_before_all_hooks - @before_all.try &.each &.call - end - - protected def after_all(&block) - (@after_all ||= [] of ->) << block - end - - protected def run_after_all_hooks - @after_all.try &.reverse_each &.call - end - - protected def around_each(&block : Example::Procsy ->) - (@around_each ||= [] of Example::Procsy ->) << block - end - protected def run_around_each_hooks(procsy : Example::Procsy) : Bool ran = @parent.run_around_each_hooks(Example::Procsy.new(procsy.example) do if @around_each @@ -353,54 +358,6 @@ module Spec end) ran || internal_run_around_each_hooks(procsy) end - - protected def internal_run_around_each_hooks(procsy : Example::Procsy) : Bool - around_each = @around_each - return false unless around_each - - run_around_each_hook(around_each, procsy, 0) - true - end - - protected def run_around_each_hook(around_each, procsy, index) : Nil - around_each[index].call( - if index == around_each.size - 1 - # If we don't have any more hooks after this one, call the procsy - procsy - else - # Otherwise, create a procsy that will invoke the next hook - Example::Procsy.new(procsy.example) do - run_around_each_hook(around_each, procsy, index + 1) - end - end - ) - end - - protected def around_all(&block : ExampleGroup::Procsy ->) - (@around_all ||= [] of ExampleGroup::Procsy ->) << block - end - - protected def run_around_all_hooks(procsy : ExampleGroup::Procsy) : Bool - around_all = @around_all - return false unless around_all - - run_around_all_hook(around_all, procsy, 0) - true - end - - protected def run_around_all_hook(around_all, procsy, index) : Nil - around_all[index].call( - if index == around_all.size - 1 - # If we don't have any more hooks after this one, call the procsy - procsy - else - # Otherwise, create a procsy that will invoke the next hook - ExampleGroup::Procsy.new(procsy.example_group) do - run_around_all_hook(around_all, procsy, index + 1) - end - end - ) - end end end diff --git a/src/spec/dsl.cr b/src/spec/dsl.cr index bbdf8666bccc..bc199149a01c 100644 --- a/src/spec/dsl.cr +++ b/src/spec/dsl.cr @@ -53,6 +53,10 @@ module Spec class AssertionFailed < SpecError end + # :nodoc: + class ExamplePending < SpecError + end + # :nodoc: class NestingSpecError < SpecError end @@ -162,7 +166,7 @@ module Spec class_property? focus = false # Instructs the spec runner to execute the given block - # before each spec, regardless of where this method is invoked. + # before each spec in the spec suite. # # If multiple blocks are registered they run in the order # that they are given. @@ -176,12 +180,11 @@ module Spec # # will print, just before each spec, 1 and then 2. def self.before_each(&block) - before_each = @@before_each ||= [] of -> - before_each << block + root_context.before_each(&block) end # Instructs the spec runner to execute the given block - # after each spec, regardless of where this method is invoked. + # after each spec spec in the spec suite. # # If multiple blocks are registered they run in the reversed # order that they are given. @@ -195,8 +198,7 @@ module Spec # # will print, just after each spec, 2 and then 1. def self.after_each(&block) - after_each = @@after_each ||= [] of -> - after_each << block + root_context.after_each(&block) end # Instructs the spec runner to execute the given block @@ -214,8 +216,7 @@ module Spec # # will print, just before the spec suite starts, 1 and then 2. def self.before_suite(&block) - before_suite = @@before_suite ||= [] of -> - before_suite << block + root_context.before_all(&block) end # Instructs the spec runner to execute the given block @@ -233,28 +234,31 @@ module Spec # # will print, just after the spec suite ends, 2 and then 1. def self.after_suite(&block) - after_suite = @@after_suite ||= [] of -> - after_suite << block - end - - # :nodoc: - def self.run_before_each_hooks - @@before_each.try &.each &.call - end - - # :nodoc: - def self.run_after_each_hooks - @@after_each.try &.reverse_each &.call - end - - # :nodoc: - def self.run_before_suite_hooks - @@before_suite.try &.each &.call + root_context.after_all(&block) end - # :nodoc: - def self.run_after_suite_hooks - @@after_suite.try &.reverse_each &.call + # Instructs the spec runner to execute the given block when each spec in the + # spec suite runs. + # + # The block must call `run` on the given `Example::Procsy` object. + # + # If multiple blocks are registered they run in the reversed + # order that they are given. + # + # ``` + # require "spec" + # + # Spec.around_each do |example| + # puts "runs before each sample" + # example.run + # puts "runs after each sample" + # end + # + # it { } + # it { } + # ``` + def self.around_each(&block : Example::Procsy ->) + root_context.around_each(&block) end @@start_time : Time::Span? = nil @@ -267,9 +271,7 @@ module Spec log_setup maybe_randomize run_filters - run_before_suite_hooks root_context.run - run_after_suite_hooks ensure finish_run end diff --git a/src/spec/example.cr b/src/spec/example.cr index 199d20be2671..01f46a6fae62 100644 --- a/src/spec/example.cr +++ b/src/spec/example.cr @@ -29,13 +29,9 @@ module Spec non_nil_block = block start = Time.monotonic - Spec.run_before_each_hooks - ran = @parent.run_around_each_hooks(Example::Procsy.new(self) { internal_run(start, non_nil_block) }) ran || internal_run(start, non_nil_block) - Spec.run_after_each_hooks - # We do this to give a chance for signals (like CTRL+C) to be handled, # which currently are only handled when there's a fiber switch # (IO stuff, sleep, etc.). Without it the user might wait more than needed @@ -51,6 +47,8 @@ module Spec rescue ex : Spec::AssertionFailed @parent.report(:fail, description, file, line, Time.monotonic - start, ex) Spec.abort! if Spec.fail_fast? + rescue ex : Spec::ExamplePending + @parent.report(:pending, description, file, line, Time.monotonic - start) rescue ex @parent.report(:error, description, file, line, Time.monotonic - start, ex) Spec.abort! if Spec.fail_fast? diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index 168facbfb42f..78ac5a47d53c 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -380,7 +380,7 @@ module Spec def expect_raises(klass : T.class, message : String | Regex | Nil = nil, file = __FILE__, line = __LINE__) forall T yield rescue ex : T - # We usually bubble Spec::AssertaionFailed, unless this is the expected exception + # We usually bubble Spec::AssertionFailed, unless this is the expected exception if ex.is_a?(Spec::AssertionFailed) && klass != Spec::AssertionFailed raise ex end diff --git a/src/spec/formatter.cr b/src/spec/formatter.cr index e031204e109e..51b355ec075f 100644 --- a/src/spec/formatter.cr +++ b/src/spec/formatter.cr @@ -25,11 +25,31 @@ module Spec # :nodoc: class DotFormatter < Formatter + @count = 0 + @split = 0 + + def initialize(*args) + super + + if split = ENV["SPEC_SPLIT_DOTS"]? + @split = split.to_i + end + end + def report(result) @io << Spec.color(LETTERS[result.kind], result.kind) + split_lines @io.flush end + private def split_lines + return unless @split > 0 + if (@count += 1) >= @split + @io.puts + @count = 0 + end + end + def finish(elapsed_time, aborted) @io.puts end diff --git a/src/spec/methods.cr b/src/spec/methods.cr index c86f0f257fae..ebde51f7023a 100644 --- a/src/spec/methods.cr +++ b/src/spec/methods.cr @@ -6,6 +6,8 @@ module Spec::Methods # # Example: # ``` + # require "spec" + # # describe "Int32" do # describe "+" do # it "adds" { (1 + 1).should eq 2 } @@ -35,6 +37,8 @@ module Spec::Methods # # Example: # ``` + # require "spec" + # # it "adds" { (1 + 1).should eq 2 } # ``` # @@ -52,6 +56,8 @@ module Spec::Methods # # Example: # ``` + # require "spec" + # # pending "check cat" { cat.alive? } # ``` # @@ -76,78 +82,225 @@ module Spec::Methods raise Spec::AssertionFailed.new(msg, file, line) end - # Executes the given block before each spec runs. + # Marks the current example pending + # + # In case an example needs to be pending on some condition that requires executing it, + # this allows to mark it as such rather than letting it fail or never run. + # + # ``` + # require "spec" + # + # it "test git" do + # cmd = Process.find_executable("git") + # pending!("git is not available") unless cmd + # cmd.ends_with?("git").should be_true + # end + # ``` + def pending!(msg = "Cannot run example", file = __FILE__, line = __LINE__) + raise Spec::ExamplePending.new(msg, file, line) + end + + # Executes the given block before each spec in the current context runs. + # + # A context is defined by `describe` or `context` blocks, or outside of them + # it's the root context. Nested contexts inherit the `*_each` blocks of + # their ancestors. + # + # If multiple blocks are registered for the same spec, the blocks defined in + # the outermost context go first. Blocks on the same context are executed in + # order of definition. + # + # ``` + # require "spec" + # + # it "sample_a" { } + # + # describe "nested_context" do + # before_each do + # puts "runs before sample_b" + # end + # + # it "sample_b" { } + # end + # ``` def before_each(&block) - Spec.root_context.before_each(&block) + if Spec.current_context.is_a?(RootContext) + raise "Can't call `before_each` outside of a describe/context" + end + Spec.current_context.before_each(&block) end - # Executes the given block after each spec runs. + # Executes the given block after each spec in the current context runs. + # + # A context is defined by `describe` or `context` blocks, or outside of them + # it's the root context. Nested contexts inherit the `*_each` blocks of + # their ancestors. + # + # If multiple blocks are registered for the same spec, the blocks defined in + # the outermost context go first. Blocks on the same context are executed in + # order of definition. + # + # ``` + # require "spec" + # + # it "sample_a" { } + # + # describe "nested_context" do + # after_each do + # puts "runs after sample_b" + # end + # + # it "sample_b" { } + # end + # ``` def after_each(&block) - Spec.root_context.after_each(&block) + if Spec.current_context.is_a?(RootContext) + raise "Can't call `after_each` outside of a describe/context" + end + Spec.current_context.after_each(&block) end - # Executes the given block before all specs in a given - # `description` or `context` run. + # Executes the given block before the first spec in the current context runs. + # + # A context is defined by `describe` or `context` blocks, or outside of them + # it's the root context. + # This is independent of the source location the specs and this hook are + # defined. + # + # If multiple blocks are registered on the same context, they are executed in + # order of definition. + # + # ``` + # require "spec" + # + # it "sample_a" { } + # + # describe "nested_context" do + # before_all do + # puts "runs at start of nested_context" + # end + # + # it "sample_b" { } + # end + # ``` def before_all(&block) - Spec.root_context.before_all(&block) + if Spec.current_context.is_a?(RootContext) + raise "Can't call `before_all` outside of a describe/context" + end + Spec.current_context.before_all(&block) end - # Executes the given block after all specs in a given - # `description` or `context` run. + # Executes the given block after the last spec in the current context runs. + # + # A context is defined by `describe` or `context` blocks, or outside of them + # it's the root context. + # This is independent of the source location the specs and this hook are + # defined. + # + # If multiple blocks are registered on the same context, they are executed in + # order of definition. + # + # ``` + # require "spec" + # + # it "sample_a" { } + # + # describe "nested_context" do + # after_all do + # puts "runs at end of nested_context" + # end + # + # it "sample_b" { } + # end + # ``` def after_all(&block) - Spec.root_context.after_all(&block) + if Spec.current_context.is_a?(RootContext) + raise "Can't call `after_all` outside of a describe/context" + end + Spec.current_context.after_all(&block) end - # Executes the given block when each spec runs. + # Executes the given block when each spec in the current context runs. # # The block must call `run` on the given `Example::Procsy` # object. # - # For example: + # This is essentially a `before_each` and `after_each` hook combined into one. + # It is useful for example when setup and teardown steps need shared state. + # + # A context is defined by `describe` or `context` blocks, or outside of them + # it's the root context. Nested contexts inherit the `*_each` blocks of + # their ancestors. + # + # If multiple blocks are registered for the same spec, the blocks defined in + # the outermost context go first. Blocks on the same context are executed in + # order of definition. # # ``` # require "spec" # - # describe "something" do + # it "sample_a" { } + # + # describe "nested_context" do # around_each do |example| - # puts "before example runs" + # puts "runs before sample_b" # example.run - # puts "after example runs" + # puts "runs after sample_b" # end # - # it "tests something" do - # # ... - # end + # it "sample_b" { } # end # ``` def around_each(&block : Example::Procsy ->) - Spec.root_context.around_each(&block) + if Spec.current_context.is_a?(RootContext) + raise "Can't call `around_each` outside of a describe/context" + end + Spec.current_context.around_each(&block) end - # Executes the given block when each `describe` or `context` runs. + # Executes the given block when the current context runs. # # The block must call `run` on the given `Context::Procsy` # object. # - # For example: + # This is essentially a `before_all` and `after_all` hook combined into one. + # It is useful for example when setup and teardown steps need shared state. + # + # A context is defined by `describe` or `context` blocks. This hook does not + # work outside such a block (i.e. in the root context). + # + # If multiple blocks are registered for the same spec, the blocks defined in + # the outermost context go first. Blocks on the same context are executed in + # order of definition. # # ``` # require "spec" # - # describe "something" do - # around_all do |context| - # puts "before describe runs" + # describe "main_context" do + # around_each do |example| + # puts "runs at beginning of main_context" # example.run - # puts "after describe runs" + # puts "runs at end of main_context" # end # - # it "tests something" do - # # ... + # it "sample_a" { } + # + # describe "nested_context" do + # around_each do |example| + # puts "runs at beginning of nested_context" + # example.run + # puts "runs at end of nested_context" + # end + # + # it "sample_b" { } # end # end # ``` def around_all(&block : ExampleGroup::Procsy ->) - Spec.root_context.around_all(&block) + if Spec.current_context.is_a?(RootContext) + raise "Can't call `around_all` outside of a describe/context" + end + Spec.current_context.around_all(&block) end end diff --git a/src/static_array.cr b/src/static_array.cr index 40936688fa16..646b31da09a0 100644 --- a/src/static_array.cr +++ b/src/static_array.cr @@ -267,7 +267,7 @@ struct StaticArray(T, N) # ``` def to_s(io : IO) : Nil io << "StaticArray[" - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << ']' end diff --git a/src/string.cr b/src/string.cr index e057e08ee2b1..781ed9e6d118 100644 --- a/src/string.cr +++ b/src/string.cr @@ -191,6 +191,8 @@ class String # String.new(ptr) # => "abcd" # ``` def self.new(chars : UInt8*) + raise ArgumentError.new("Cannot create a string with a null pointer") if chars.null? + new(chars, LibC.strlen(chars)) end @@ -206,6 +208,8 @@ class String # String.new(ptr, 2) # => "ab" # ``` def self.new(chars : UInt8*, bytesize, size = 0) + raise ArgumentError.new("Cannot create a string with a null pointer") if chars.null? + # Avoid allocating memory for the empty string return "" if bytesize == 0 @@ -784,28 +788,11 @@ class String # Like `#[Int, Int]` but returns `nil` if the *start* index is out of bounds. def []?(start : Int, count : Int) raise ArgumentError.new "Negative count: #{count}" if count < 0 - return byte_slice?(start, count) if ascii_only? + return byte_slice?(start, count) if single_byte_optimizable? start += size if start < 0 - start_pos = nil - end_pos = nil - - reader = Char::Reader.new(self) - i = 0 - - reader.each do |char| - if i == start - start_pos = reader.pos - elsif count >= 0 && i == start + count - end_pos = reader.pos - i += 1 - break - end - i += 1 - end - - end_pos ||= reader.pos + start_pos, end_pos, end_index = find_start_end_and_index(start, count) if start_pos return "" if count == 0 @@ -817,7 +804,7 @@ class String buffer.copy_from(to_unsafe + start_pos, count) {count, 0} end - elsif start == i + elsif start == end_index "" end end @@ -880,7 +867,7 @@ class String # "hello".char_at(-6) { 'x' } # => 'x' # ``` def char_at(index : Int, &) - if ascii_only? + if single_byte_optimizable? byte = byte_at?(index) if byte return byte < 0x80 ? byte.unsafe_chr : Char::REPLACEMENT @@ -900,6 +887,168 @@ class String end end + # Returns a new string that results from deleting characters + # at the given range. + # + # ``` + # "abcdef".delete_at(1..3) # => "aef" + # ``` + # + # Negative indices can be used to start counting from the end of the string: + # + # ``` + # "abcdef".delete_at(-3..-2) # => "abcf" + # ``` + # + # Raises `IndexError` if any index is outside the bounds of this string. + def delete_at(range : Range) + delete_at(*Indexable.range_to_index_and_count(range, size)) + end + + # Returns a new string that results from deleting the character + # at the given *index*. + # + # ``` + # "abcde".delete_at(0) # => "bcde" + # "abcde".delete_at(2) # => "abde" + # "abcde".delete_at(4) # => "abcd" + # ``` + # + # A negative *index* counts from the end of the string: + # + # ``` + # "abcde".delete_at(-2) # => "abce" + # ``` + # + # If *index* is outside the bounds of the string, `IndexError` is raised. + def delete_at(index : Int) : String + index += size if index < 0 + + byte_index = char_index_to_byte_index(index) + if byte_index && byte_index < @bytesize + char_bytesize = char_bytesize_at(byte_index) + + new_bytesize = self.bytesize - char_bytesize + String.new(new_bytesize) do |buffer| + # Copy left part + buffer.copy_from(to_unsafe, byte_index) + + # Copy right part + (buffer + byte_index).copy_from( + to_unsafe + byte_index + char_bytesize, + self.bytesize - byte_index - char_bytesize, + ) + + {new_bytesize, size - 1} + end + else + raise IndexError.new + end + end + + # Returns a new string that results from deleting *count* characters + # starting at *index*. + # + # ``` + # "abcdefg".delete_at(1, 3) # => "aefg" + # ``` + # + # Deleting more characters than those in the string is valid, and just + # results in deleting up to the last character: + # + # ``` + # "abcdefg".delete_at(3, 10) # => "abc" + # ``` + # + # A negative *index* counts from the end of the string: + # + # ``` + # "abcdefg".delete_at(-3, 2) # => "abcdg" + # ``` + # + # If *count* is negative, `ArgumentError` is raised. + # + # If *index* is outside the bounds of the string, `ArgumentError` + # is raised. + # + # However, *index* can be the position that is exactly the end of the string: + # + # ``` + # "abcd".delete_at(4, 3) # => "abcd" + # ``` + def delete_at(index : Int, count : Int) : String + raise ArgumentError.new "Negative count: #{count}" if count < 0 + + index += size if index < 0 + unless 0 <= index <= size + raise IndexError.new + end + + count = Math.min(count, size - index) + + case count + when 0 + return self + when size + return "" + else + if single_byte_optimizable? + byte_delete_at(index, count, count) + else + unicode_delete_at(index, count) + end + end + end + + private def byte_delete_at(start, count, byte_count) + new_bytesize = bytesize - byte_count + String.new(new_bytesize) do |buffer| + # Copy left part + buffer.copy_from(to_unsafe, start) + + # Copy right part + (buffer + start).copy_from( + to_unsafe + start + byte_count, + bytesize - start - byte_count, + ) + + {new_bytesize, size - count} + end + end + + private def unicode_delete_at(start, count) + start_pos, end_pos, _ = find_start_end_and_index(start, count) + + # That start is in bounds was already verified in `delete_at` + start_pos = start_pos.not_nil! + + byte_count = end_pos - start_pos.not_nil! + byte_delete_at(start_pos, count, byte_count) + end + + private def find_start_end_and_index(start, count) + start_pos = nil + end_pos = nil + + reader = Char::Reader.new(self) + i = 0 + + reader.each do |char| + if i == start + start_pos = reader.pos + elsif i == start + count + end_pos = reader.pos + i += 1 + break + end + i += 1 + end + + end_pos ||= reader.pos + + {start_pos, end_pos, i} + end + # Returns a new string built from *count* bytes starting at *start* byte. # # *start* can can be negative to start counting @@ -950,7 +1099,7 @@ class String raise ArgumentError.new "Negative count" if count < 0 start += bytesize if start < 0 - single_byte_optimizable = ascii_only? + single_byte_optimizable = single_byte_optimizable? if 0 <= start < bytesize count = bytesize - start if start + count > bytesize @@ -1056,60 +1205,77 @@ class String end end - def unsafe_byte_at(index) + # Returns the byte at the given *index* without bounds checking. + def unsafe_byte_at(index : Int) : UInt8 to_unsafe[index] end - # Returns a new `String` with each uppercase letter replaced with its lowercase - # counterpart. + # Returns a new `String` with each uppercase letter replaced with its lowercase counterpart. # # ``` # "hEllO".downcase # => "hello" # ``` - def downcase(options = Unicode::CaseOptions::None) + def downcase(options : Unicode::CaseOptions = :none) : String return self if empty? - if ascii_only? && (options.none? || options.ascii?) - String.new(bytesize) do |buffer| + if single_byte_optimizable? && (options.none? || options.ascii?) + return String.new(bytesize) do |buffer| bytesize.times do |i| - buffer[i] = to_unsafe[i].unsafe_chr.downcase.ord.to_u8 + buffer[i] = unsafe_byte_at(i).unsafe_chr.downcase.ord.to_u8 end {@bytesize, @length} end - else - String.build(bytesize) do |io| - each_char do |char| - char.downcase(options) do |res| - io << res - end - end + end + + String.build(bytesize) { |io| downcase io, options } + end + + # Writes a downcased version of `self` to the given *io*. + # + # ``` + # io = IO::Memory.new + # "hEllO".downcase io + # io.to_s # => hello + # ``` + def downcase(io : IO, options : Unicode::CaseOptions = :none) : Nil + each_char do |char| + char.downcase(options) do |res| + io << res end end end - # Returns a new `String` with each lowercase letter replaced with its uppercase - # counterpart. + # Returns a new `String` with each lowercase letter replaced with its uppercase counterpart. # # ``` # "hEllO".upcase # => "HELLO" # ``` - def upcase(options = Unicode::CaseOptions::None) + def upcase(options : Unicode::CaseOptions = :none) : String return self if empty? - if ascii_only? && (options.none? || options.ascii?) - String.new(bytesize) do |buffer| + if single_byte_optimizable? && (options.none? || options.ascii?) + return String.new(bytesize) do |buffer| bytesize.times do |i| - buffer[i] = to_unsafe[i].unsafe_chr.upcase.ord.to_u8 + buffer[i] = unsafe_byte_at(i).unsafe_chr.upcase.ord.to_u8 end {@bytesize, @length} end - else - String.build(bytesize) do |io| - each_char do |char| - char.upcase(options) do |res| - io << res - end - end + end + + String.build(bytesize) { |io| upcase io, options } + end + + # Writes a upcased version of `self` to the given *io*. + # + # ``` + # io = IO::Memory.new + # "hEllO".upcase io + # io.to_s # => HELLO + # ``` + def upcase(io : IO, options : Unicode::CaseOptions = :none) : Nil + each_char do |char| + char.upcase(options) do |res| + io << res end end end @@ -1120,29 +1286,40 @@ class String # ``` # "hEllO".capitalize # => "Hello" # ``` - def capitalize(options = Unicode::CaseOptions::None) + def capitalize(options : Unicode::CaseOptions = :none) : String return self if empty? - if ascii_only? && (options.none? || options.ascii?) - String.new(bytesize) do |buffer| + if single_byte_optimizable? && (options.none? || options.ascii?) + return String.new(bytesize) do |buffer| bytesize.times do |i| - if i == 0 - buffer[i] = to_unsafe[i].unsafe_chr.upcase.ord.to_u8 - else - buffer[i] = to_unsafe[i].unsafe_chr.downcase.ord.to_u8 - end + byte = if i.zero? + unsafe_byte_at(i).unsafe_chr.upcase.ord.to_u8 + else + unsafe_byte_at(i).unsafe_chr.downcase.ord.to_u8 + end + + buffer[i] = byte end {@bytesize, @length} end - else - String.build(bytesize) do |io| - each_char_with_index do |char, i| - if i == 0 - char.upcase(options) { |c| io << c } - else - char.downcase(options) { |c| io << c } - end - end + end + + String.build(bytesize) { |io| capitalize io, options } + end + + # Writes a capitalized version of `self` to the given *io*. + # + # ``` + # io = IO::Memory.new + # "hEllO".capitalize io + # io.to_s # => Hello + # ``` + def capitalize(io : IO, options : Unicode::CaseOptions = :none) : Nil + each_char_with_index do |char, i| + if i.zero? + char.upcase(options) { |c| io << c } + else + char.downcase(options) { |c| io << c } end end end @@ -1155,28 +1332,40 @@ class String # " spaces before".titleize # => " Spaces Before" # "x-men: the last stand".titleize # => "X-men: The Last Stand" # ``` - def titleize(options = Unicode::CaseOptions::None) + def titleize(options : Unicode::CaseOptions = :none) : String return self if empty? - upcase_next = true - if ascii_only? && (options.none? || options.ascii?) - String.new(bytesize) do |buffer| + if single_byte_optimizable? && (options.none? || options.ascii?) + upcase_next = true + + return String.new(bytesize) do |buffer| bytesize.times do |i| - char = to_unsafe[i].unsafe_chr + char = unsafe_byte_at(i).unsafe_chr replaced_char = upcase_next ? char.upcase : char.downcase buffer[i] = replaced_char.ord.to_u8 upcase_next = char.whitespace? end {@bytesize, @length} end - else - String.build(bytesize) do |io| - each_char_with_index do |char, i| - replaced_char = upcase_next ? char.upcase(options) : char.downcase(options) - io << replaced_char - upcase_next = char.whitespace? - end - end + end + + String.build(bytesize) { |io| titleize io, options } + end + + # Writes a titleized version of `self` to the given *io*. + # + # ``` + # io = IO::Memory.new + # "x-men: the last stand".titleize io + # io.to_s # => X-men: The Last Stand + # ``` + def titleize(io : IO, options : Unicode::CaseOptions = :none) : Nil + upcase_next = true + + each_char_with_index do |char, i| + replaced_char = upcase_next ? char.upcase(options) : char.downcase(options) + io << replaced_char + upcase_next = char.whitespace? end end @@ -1273,7 +1462,7 @@ class String def lchop? : String? return if empty? - if ascii_only? + if single_byte_optimizable? unsafe_byte_slice_string(1, bytesize - 1) else reader = Char::Reader.new(self) @@ -1331,9 +1520,9 @@ class String # "".rchop? # => nil # ``` def rchop? : String? - return if bytesize <= 1 + return if empty? - if to_unsafe[bytesize - 1] < 128 || ascii_only? + if to_unsafe[bytesize - 1] < 0x80 || single_byte_optimizable? return unsafe_byte_slice_string(0, bytesize - 1) end @@ -1461,7 +1650,7 @@ class String bytes, count = String.char_bytes_and_bytesize(other) new_bytesize = bytesize + count - new_size = (ascii_only? && other.ascii?) ? new_bytesize : 0 + new_size = (single_byte_optimizable? && other.ascii?) ? new_bytesize : 0 insert_impl(byte_index, bytes.to_unsafe, count, new_bytesize, new_size) end @@ -1490,7 +1679,7 @@ class String raise IndexError.new unless byte_index new_bytesize = bytesize + other.bytesize - new_size = ascii_only? && other.ascii_only? ? new_bytesize : 0 + new_size = single_byte_optimizable? && other.single_byte_optimizable? ? new_bytesize : 0 insert_impl(byte_index, other.to_unsafe, other.bytesize, new_bytesize, new_size) end @@ -2095,7 +2284,7 @@ class String buffer.value = byte buffer += 1 end - {buffer, ascii_only? ? bytesize - (to_index - from_index) + 1 : 0} + {buffer, single_byte_optimizable? ? bytesize - (to_index - from_index) + 1 : 0} end end @@ -2638,7 +2827,7 @@ class String def compare(other : String, case_insensitive = false, options = Unicode::CaseOptions::None) return self <=> other unless case_insensitive - if ascii_only? && other.ascii_only? + if single_byte_optimizable? && other.single_byte_optimizable? position = 0 while position < bytesize && position < other.bytesize @@ -2807,7 +2996,7 @@ class String # ``` def index(search : Char, offset = 0) # If it's ASCII we can delegate to slice - if search.ascii? && ascii_only? + if search.ascii? && single_byte_optimizable? return to_slice.index(search.ord.to_u8, offset) end @@ -2845,16 +3034,8 @@ class String pointer = to_unsafe end_pointer = pointer + bytesize while char_index < offset && pointer < end_pointer - byte = pointer.value - if byte < 0x80 - pointer += 1 - elsif byte < 0xe0 - pointer += 2 - elsif byte < 0xf0 - pointer += 3 - else - pointer += 4 - end + char_bytesize = String.char_bytesize_at(pointer) + pointer += char_bytesize char_index += 1 end @@ -2878,18 +3059,14 @@ class String return if pointer >= end_pointer byte = head_pointer.value - - # update a rolling hash of this text (heystack) - # thanks @MaxLap for suggesting this loop reduction - if byte < 0x80 - update_hash 1 - elsif byte < 0xe0 - update_hash 2 - elsif byte < 0xf0 - update_hash 3 - else - update_hash 4 + char_bytesize = String.char_bytesize_at(head_pointer) + case char_bytesize + when 1 then update_hash 1 + when 2 then update_hash 2 + when 3 then update_hash 3 + else update_hash 4 end + char_index += 1 end end @@ -2914,7 +3091,7 @@ class String # ``` def rindex(search : Char, offset = size - 1) # If it's ASCII we can delegate to slice - if search.ascii? && ascii_only? + if search.ascii? && single_byte_optimizable? return to_slice.rindex(search.ord.to_u8, offset) end @@ -2995,7 +3172,7 @@ class String end # :ditto: - def rindex(search : Regex, offset = size - 1) + def rindex(search : Regex, offset = size) offset += size if offset < 0 return nil unless 0 <= offset <= size @@ -3202,7 +3379,7 @@ class String # "こんにちは".char_index_to_byte_index(5) # => 15 # ``` def char_index_to_byte_index(index) - if ascii_only? + if single_byte_optimizable? return 0 <= index <= bytesize ? index : nil end @@ -3218,7 +3395,7 @@ class String # It is valid to pass `#bytesize` to *index*, and in this case the answer # will be the size of this string. def byte_index_to_char_index(index) - if ascii_only? + if single_byte_optimizable? return 0 <= index <= bytesize ? index : nil end @@ -3290,7 +3467,7 @@ class String end yielded = 0 - single_byte_optimizable = ascii_only? + single_byte_optimizable = single_byte_optimizable? index = 0 i = 0 looking_for_space = false @@ -3481,7 +3658,7 @@ class String byte_offset = 0 separator_bytesize = separator.bytesize - single_byte_optimizable = ascii_only? + single_byte_optimizable = single_byte_optimizable? i = 0 stop = bytesize - separator.bytesize + 1 @@ -3571,7 +3748,7 @@ class String while match = separator.match_at_byte_index(self, match_offset) index = match.byte_begin(0) - match_bytesize = match[0].bytesize + match_bytesize = match.byte_end(0) - index next_offset = index + match_bytesize if next_offset == slice_offset @@ -3672,74 +3849,83 @@ class String # "3.14IsPi".underscore # => "3.14_is_pi" # "InterestingImage".underscore(Unicode::CaseOptions::Turkic) # => "ınteresting_ımage" # ``` - def underscore(options : Unicode::CaseOptions = Unicode::CaseOptions::None) + def underscore(options : Unicode::CaseOptions = :none) : String + String.build(bytesize + 10) { |io| underscore io, options } + end + + # Writes an underscored version of `self` to the given *io*. + # + # ``` + # io = IO::Memory.new + # "DoesWhatItSaysOnTheTin".underscore io + # io.to_s # => "does_what_it_says_on_the_tin" + # ``` + def underscore(io : IO, options : Unicode::CaseOptions = :none) : Nil first = true last_is_downcase = false last_is_upcase = false last_is_digit = false - mem = nil + mem : Char? = nil - String.build(bytesize + 10) do |str| - each_char do |char| - digit = char.ascii_number? + each_char do |char| + digit = char.ascii_number? - if options.none? - downcase = digit || char.ascii_lowercase? - upcase = char.ascii_uppercase? - else - downcase = digit || char.lowercase? - upcase = char.uppercase? - end + if options.none? + downcase = digit || char.ascii_lowercase? + upcase = char.ascii_uppercase? + else + downcase = digit || char.lowercase? + upcase = char.uppercase? + end - if first - str << char.downcase(options) - elsif last_is_downcase && upcase - if mem - # This is the case of A1Bcd, we need to put 'mem' (not to need to convert as downcase - # ^ - # because 'mem' is digit surely) before putting this char as downcase. - str << mem - mem = nil - end - # This is the case of AbcDe, we need to put an underscore before the 'D' - # ^ - str << '_' - str << char.downcase(options) - elsif (last_is_upcase || last_is_digit) && (upcase || digit) - # This is the case of 1) A1Bcd, 2) A1BCd or 3) A1B_cd:if the next char is upcase (case 1) we need - # ^ ^ ^ - # 1) we need to append this char as downcase - # 2) we need to append an underscore and then the char as downcase, so we save this char - # in 'mem' and decide later - # 3) we need to append this char as downcase and then a single underscore - if mem - # case 2 - str << mem.downcase(options) - end - mem = char - else - if mem - if char == '_' - # case 3 - elsif last_is_upcase && downcase - # case 1 - str << '_' - end - str << mem.downcase(options) - mem = nil + if first + io << char.downcase(options) + elsif last_is_downcase && upcase + if mem + # This is the case of A1Bcd, we need to put 'mem' (not to need to convert as downcase + # ^ + # because 'mem' is digit surely) before putting this char as downcase. + io << mem + mem = nil + end + # This is the case of AbcDe, we need to put an underscore before the 'D' + # ^ + io << '_' + io << char.downcase(options) + elsif (last_is_upcase || last_is_digit) && (upcase || digit) + # This is the case of 1) A1Bcd, 2) A1BCd or 3) A1B_cd:if the next char is upcase (case 1) we need + # ^ ^ ^ + # 1) we need to append this char as downcase + # 2) we need to append an underscore and then the char as downcase, so we save this char + # in 'mem' and decide later + # 3) we need to append this char as downcase and then a single underscore + if mem + # case 2 + io << mem.downcase(options) + end + mem = char + else + if mem + if char == '_' + # case 3 + elsif last_is_upcase && downcase + # case 1 + io << '_' end - - str << char.downcase(options) + io << mem.downcase(options) + mem = nil end - last_is_downcase = downcase - last_is_upcase = upcase - last_is_digit = digit - first = false + io << char.downcase(options) end - str << mem.downcase(options) if mem + last_is_downcase = downcase + last_is_upcase = upcase + last_is_digit = digit + first = false end + + io << mem.downcase(options) if mem end # Converts underscores to camelcase boundaries. @@ -3751,26 +3937,37 @@ class String # "empire_state_building".camelcase(lower: true) # => "empireStateBuilding" # "isolated_integer".camelcase(options: Unicode::CaseOptions::Turkic) # => "İsolatedİnteger" # ``` - def camelcase(options : Unicode::CaseOptions = Unicode::CaseOptions::None, *, lower : Bool = false) + def camelcase(options : Unicode::CaseOptions = Unicode::CaseOptions::None, *, lower : Bool = false) : String return self if empty? + String.build(bytesize) { |io| camelcase io, options, lower: lower } + end + + # Writes an camelcased version of `self` to the given *io*. + # + # If *lower* is true, lower camelcase will be written (the first letter is downcased). + # + # ``` + # io = IO::Memory.new + # "eiffel_tower".camelcase io + # io.to_s # => "EiffelTower" + # ``` + def camelcase(io : IO, options : Unicode::CaseOptions = Unicode::CaseOptions::None, *, lower : Bool = false) : Nil first = true last_is_underscore = false - String.build(bytesize) do |str| - each_char do |char| - if first - str << (lower ? char.downcase(options) : char.upcase(options)) - elsif char == '_' - last_is_underscore = true - elsif last_is_underscore - str << char.upcase(options) - last_is_underscore = false - else - str << char - end - first = false + each_char do |char| + if first + io << (lower ? char.downcase(options) : char.upcase(options)) + elsif char == '_' + last_is_underscore = true + elsif last_is_underscore + io << char.upcase(options) + last_is_underscore = false + else + io << char end + first = false end end @@ -3783,7 +3980,7 @@ class String def reverse return self if bytesize <= 1 - if ascii_only? + if single_byte_optimizable? String.new(bytesize) do |buffer| bytesize.times do |i| buffer[i] = self.to_unsafe[bytesize - i - 1] @@ -3796,9 +3993,10 @@ class String String.new(bytesize) do |buffer| buffer += bytesize scan(/\X/) do |match| - grapheme = match[0] - buffer -= grapheme.bytesize - buffer.copy_from(grapheme.to_unsafe, grapheme.bytesize) + match_begin = match.byte_begin(0) + match_bytesize = match.byte_end(0) - match_begin + buffer -= match_bytesize + buffer.copy_from(to_unsafe + match_begin, match_bytesize) end {@bytesize, @length} end @@ -3824,8 +4022,9 @@ class String # "Purple".ljust(8, io) # io.to_s # => "Purple " # ``` + @[Deprecated("Use `#ljust(io :IO, len : Int, char : Char = ' ')` instead")] def ljust(len : Int, io : IO) : Nil - ljust(len, ' ', io) + ljust(io, len) end # Adds instances of *char* to right of the string until it is at least size of *len*, @@ -3833,14 +4032,27 @@ class String # # ``` # io = IO::Memory.new - # "Purple".ljust(8, '-', io) + # "Purple".ljust(io, 8, '-') # io.to_s # => "Purple--" # ``` - def ljust(len : Int, char : Char, io : IO) : Nil + def ljust(io : IO, len : Int, char : Char = ' ') : Nil io << self (len - size).times { io << char } end + # Adds instances of *char* to right of the string until it is at least size of *len*, + # and then appends the result to the given IO. + # + # ``` + # io = IO::Memory.new + # "Purple".ljust(8, '-', io) + # io.to_s # => "Purple--" + # ``` + @[Deprecated("Use `#ljust(io :IO, len : Int, char : Char = ' ')` instead")] + def ljust(len : Int, char : Char, io : IO) : Nil + ljust(io, len, char) + end + # Adds instances of *char* to left of the string until it is at least size of *len*. # # ``` @@ -3860,8 +4072,9 @@ class String # "Purple".rjust(8, io) # io.to_s # => " Purple" # ``` + @[Deprecated("Use `#rjust(io :IO, len : Int, char : Char = ' ')` instead")] def rjust(len : Int, io : IO) : Nil - rjust(len, ' ', io) + rjust(io, len) end # Adds instances of *char* to left of the string until it is at least size of *len*, @@ -3872,11 +4085,24 @@ class String # "Purple".rjust(8, '-', io) # io.to_s # => "--Purple" # ``` - def rjust(len : Int, char : Char, io : IO) : Nil + def rjust(io : IO, len : Int, char : Char = ' ') : Nil (len - size).times { io << char } io << self end + # Adds instances of *char* to left of the string until it is at least size of *len*, + # and then appends the result to the given IO. + # + # ``` + # io = IO::Memory.new + # "Purple".rjust(8, '-', io) + # io.to_s # => "--Purple" + # ``` + @[Deprecated("Use `#rjust(io :IO, len : Int, char : Char = ' ')` instead")] + def rjust(len : Int, char : Char, io : IO) : Nil + rjust(io, len, char) + end + # Adds instances of *char* to left and right of the string until it is at least size of *len*. # # ``` @@ -3897,8 +4123,9 @@ class String # "Purple".center(9, io) # io.to_s # => " Purple " # ``` + @[Deprecated("Use `#center(io :IO, len : Int, char : Char = ' ')` instead")] def center(len : Int, io : IO) : Nil - center(len, ' ', io) + center(io, len) end # Adds instances of *char* to left and right of the string until it is at least size of *len*, @@ -3909,7 +4136,7 @@ class String # "Purple".center(9, '-', io) # io.to_s # => "-Purple--" # ``` - def center(len : Int, char : Char, io : IO) : Nil + def center(io : IO, len : Int, char : Char = ' ') : Nil difference = len - size if difference <= 0 @@ -3925,6 +4152,19 @@ class String right_padding.times { io << char } end + # Adds instances of *char* to left ond right of the string until it is at least size of *len*, + # then appends the result to the given IO. + # + # ``` + # io = IO::Memory.new + # "Purple".center(9, '-', io) + # io.to_s # => "-Purple--" + # ``` + @[Deprecated("Use `#center(io :IO, len : Int, char : Char = ' ')` instead")] + def center(len : Int, char : Char, io : IO) : Nil + center(io, len, char) + end + private def just(len, char, justify) return self if size >= len @@ -4073,7 +4313,7 @@ class String index = match.byte_begin(0) $~ = match yield match - match_bytesize = match[0].bytesize + match_bytesize = match.byte_end(0) - index match_bytesize += 1 if match_bytesize == 0 byte_offset = index + match_bytesize end @@ -4123,7 +4363,7 @@ class String # array # => ['a', 'b', '☃'] # ``` def each_char : Nil - if ascii_only? + if single_byte_optimizable? each_byte do |byte| yield (byte < 0x80 ? byte.unsafe_chr : Char::REPLACEMENT) end @@ -4430,7 +4670,7 @@ class String private def dump_hex(char, io) io << "\\x" io << '0' if char < 0x0F - char.to_s(16, io, upcase: true) + char.to_s(io, 16, upcase: true) end private def dump_unicode(char, io) @@ -4439,7 +4679,7 @@ class String io << '0' if char.ord < 0x1000 io << '0' if char.ord < 0x0100 io << '0' if char.ord < 0x0010 - char.ord.to_s(16, io, upcase: true) + char.ord.to_s(io, 16, upcase: true) io << '}' if char.ord > 0xFFFF end @@ -4504,7 +4744,7 @@ class String def ends_with?(char : Char) : Bool return false unless bytesize > 0 - if char.ascii? || ascii_only? + if char.ascii? || single_byte_optimizable? return to_unsafe[bytesize - 1] == char.ord end @@ -4532,7 +4772,7 @@ class String !!($~ = /#{re}\z/.match(self)) end - # Interpolates *other* into the string using `Kernel#sprintf`. + # Interpolates *other* into the string using top-level `::sprintf`. # # ``` # "I have %d apples" % 5 # => "I have 5 apples" @@ -4571,6 +4811,18 @@ class String # "你好".ascii_only? # => false # ``` def ascii_only? + if @bytesize == size + each_byte do |byte| + return false unless byte < 0x80 + end + true + else + false + end + end + + # :nodoc: + def single_byte_optimizable? @bytesize == size end @@ -4610,43 +4862,67 @@ class String end protected def char_bytesize_at(byte_index) - first = unsafe_byte_at(byte_index) + String.char_bytesize_at(to_unsafe + byte_index) + end + + protected def self.char_bytesize_at(bytes : Pointer(UInt8)) + first = bytes.value if first < 0x80 return 1 end if first < 0xc2 - return 1 + return 1 # Invalid end - second = unsafe_byte_at(byte_index + 1) + second = bytes[1] + if (second & 0xc0) != 0x80 - return 1 + return 1 # Invalid end if first < 0xe0 return 2 end - third = unsafe_byte_at(byte_index + 2) + third = bytes[2] + if (third & 0xc0) != 0x80 - return 2 + return 1 # Invalid end if first < 0xf0 + if first == 0xe0 && second < 0xa0 + return 1 # Invalid + end + + if first == 0xed && second >= 0xa0 + return 1 # Invalid + end + return 3 end if first == 0xf0 && second < 0x90 - return 3 + return 1 # Invalid end if first == 0xf4 && second >= 0x90 - return 3 + return 1 # Invalid + end + + fourth = bytes[3] + + if (fourth & 0xc0) != 0x80 + return 1 # Invalid + end + + if first < 0xf5 + return 4 end - return 4 + 1 # Invalid end # :nodoc: diff --git a/src/string/builder.cr b/src/string/builder.cr index bd6f94b8f15c..eb187535420a 100644 --- a/src/string/builder.cr +++ b/src/string/builder.cr @@ -49,8 +49,6 @@ class String::Builder < IO slice.copy_to(@buffer + real_bytesize, count) @bytesize += count - - nil end def write_byte(byte : UInt8) diff --git a/src/string/formatter.cr b/src/string/formatter.cr index 3ac9ef6d9f02..39cf6250f7ce 100644 --- a/src/string/formatter.cr +++ b/src/string/formatter.cr @@ -229,21 +229,28 @@ struct String::Formatter(A) if flags.left_padding? if flags.padding_char == '0' - @io << '+' if flags.plus + if flags.plus + if int >= 0 + @io << '+' + else + @io << '-' + int = int.abs + end + end @io << ' ' if flags.space end pad_int int, flags end - if int > 0 + if int >= 0 unless flags.padding_char == '0' @io << '+' if flags.plus @io << ' ' if flags.space end end - int.to_s(flags.base, @io, upcase: flags.type == 'X') + int.to_s(@io, flags.base, upcase: flags.type == 'X') if flags.right_padding? pad_int int, flags @@ -312,7 +319,7 @@ struct String::Formatter(A) def pad_int(int, flags) size = int.to_s(flags.base).bytesize - size += 1 if int > 0 && (flags.plus || flags.space) + size += 1 if int >= 0 && (flags.plus || flags.space) pad size, flags end diff --git a/src/termios.cr b/src/termios.cr index 698086db9329..db34be00c8b1 100644 --- a/src/termios.cr +++ b/src/termios.cr @@ -42,7 +42,7 @@ module Termios TAB0 = LibC::TAB0 TAB3 = LibC::TAB3 end - {% elsif flag?(:openbsd) %} + {% elsif flag?(:netbsd) || flag?(:openbsd) %} @[Flags] enum OutputMode OPOST = LibC::OPOST diff --git a/src/time.cr b/src/time.cr index edd5a4bee31f..02b2a8883747 100644 --- a/src/time.cr +++ b/src/time.cr @@ -189,7 +189,7 @@ require "crystal/system/time" # computer's wall clock has changed between both calls. # # As an alternative, the operating system also provides a monotonic clock. -# Its time-line has no specfied starting point but is strictly linearly +# Its time-line has no specified starting point but is strictly linearly # increasing. # # This monotonic clock should always be used for measuring elapsed time. @@ -410,7 +410,10 @@ struct Time unless 1 <= year <= 9999 && 1 <= month <= 12 && 1 <= day <= Time.days_in_month(year, month) && - 0 <= hour <= 23 && + ( + 0 <= hour <= 23 || + (hour == 24 && minute == 0 && second == 0 && nanosecond == 0) + ) && 0 <= minute <= 59 && 0 <= second <= 59 && 0 <= nanosecond <= 999_999_999 @@ -581,7 +584,7 @@ struct Time # date `2007-04-31` which will be adjusted to `2007-04-30`. # # This operates on the local time-line, such that the local date-time - # represenations of month and year are increased by the specified amount. + # representations of month and year are increased by the specified amount. # # If the resulting date-time is ambiguous due to time zone transitions, # a correct time will be returned, but it does not guarantee which. @@ -599,7 +602,7 @@ struct Time # date `2007-04-31` which will be adjusted to `2007-04-30`. # # This operates on the local time-line, such that the local date-time - # represenations of month and year are decreased by the specified amount. + # representations of month and year are decreased by the specified amount. # # If the resulting date-time is ambiguous due to time zone transitions, # a correct time will be returned, but it does not guarantee which. @@ -1034,13 +1037,13 @@ struct Time # # The name of the location is appended unless it is a fixed zone offset. def inspect(io : IO, with_nanoseconds = true) : Nil - to_s "%F %T", io + to_s io, "%F %T" if with_nanoseconds if @nanoseconds == 0 io << ".0" else - to_s ".%N", io + to_s io, ".%N" end end @@ -1060,7 +1063,7 @@ struct Time # When the location is `UTC`, the offset is replaced with the string `UTC`. # Offset seconds are omitted if `0`. def to_s(io : IO) : Nil - to_s("%F %T ", io) + to_s(io, "%F %T ") if utc? io << "UTC" @@ -1084,10 +1087,18 @@ struct Time # Formats this `Time` according to the pattern in *format* to the given *io*. # # See `Time::Format` for details. - def to_s(format : String, io : IO) : Nil + def to_s(io : IO, format : String) : Nil Format.new(format).format(self, io) end + # Formats this `Time` according to the pattern in *format* to the given *io*. + # + # See `Time::Format` for details. + @[Deprecated("Use `#to_s(io : IO, format : String)` instead")] + def to_s(format : String, io : IO) : Nil + to_s(io, format) + end + # Format this time using the format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # # ``` @@ -1097,14 +1108,21 @@ struct Time # ISO 8601 allows some freedom over the syntax and RFC 3339 exercises that # freedom to rigidly define a fixed format intended for use in internet # protocols and standards. - def to_rfc3339 - Format::RFC_3339.format(to_utc) + # + # Number of seconds decimals can be selected with *fraction_digits*. + # Values accepted are 0 (the default, no decimals), 3 (milliseconds), 6 (microseconds) or 9 (nanoseconds). + def to_rfc3339(*, fraction_digits : Int = 0) + Format::RFC_3339.format(to_utc, fraction_digits) end # Format this time using the format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # into the given *io*. - def to_rfc3339(io : IO) - Format::RFC_3339.format(to_utc, io) + # + # + # Number of seconds decimals can be selected with *fraction_digits*. + # Values accepted are 0 (the default, no decimals), 3 (milliseconds), 6 (microseconds) or 9 (nanoseconds). + def to_rfc3339(io : IO, *, fraction_digits : Int = 0) + Format::RFC_3339.format(to_utc, io, fraction_digits) end # Parse time format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). @@ -1296,7 +1314,7 @@ struct Time if local? self else - in(Location.local) + self.in(Location.local) end end diff --git a/src/time/format/custom/rfc_3339.cr b/src/time/format/custom/rfc_3339.cr index 94a68ac9f09e..7c16669aab9a 100644 --- a/src/time/format/custom/rfc_3339.cr +++ b/src/time/format/custom/rfc_3339.cr @@ -9,14 +9,14 @@ struct Time::Format end # Formats a `Time` into the given *io*. - def self.format(time : Time, io : IO, fraction_digits = nil) + def self.format(time : Time, io : IO, fraction_digits = 0) formatter = Formatter.new(time, io) formatter.rfc_3339(fraction_digits: fraction_digits) io end # Formats a `Time` into a `String`. - def self.format(time : Time, fraction_digits = nil) + def self.format(time : Time, fraction_digits = 0) String.build do |io| format(time, io, fraction_digits: fraction_digits) end @@ -24,7 +24,7 @@ struct Time::Format end module Pattern - def rfc_3339(fraction_digits = nil) + def rfc_3339(fraction_digits = 0) year_month_day char 'T', 't', ' ' twenty_four_hour_time_with_seconds diff --git a/src/time/format/formatter.cr b/src/time/format/formatter.cr index ca52495dfa4e..cc259ca53081 100644 --- a/src/time/format/formatter.cr +++ b/src/time/format/formatter.cr @@ -147,10 +147,14 @@ struct Time::Format nanoseconds end - def second_fraction?(fraction_digits = nil) - unless time.nanosecond == 0 || fraction_digits == 0 - char '.' - second_fraction + def second_fraction?(fraction_digits : Int = 9) + case fraction_digits + when 0 + when 3 then char '.'; milliseconds + when 6 then char '.'; microseconds + when 9 then char '.'; nanoseconds + else + raise ArgumentError.new("Invalid fraction digits: #{fraction_digits}") end end diff --git a/src/time/format/parser.cr b/src/time/format/parser.cr index 3eb1e2576698..b038f837742f 100644 --- a/src/time/format/parser.cr +++ b/src/time/format/parser.cr @@ -33,11 +33,26 @@ struct Time::Format @second = 0 @nanosecond = 0 @pm = false + @hour_is_12 = false @nanosecond_offset = 0_i64 end def time(location : Location? = nil) - @hour += 12 if @pm + if @hour_is_12 + if @hour > 12 + raise ArgumentError.new("Invalid hour for 12-hour clock") + end + + if @pm + @hour += 12 unless @hour == 12 + else + if @hour == 0 + raise ArgumentError.new("Invalid hour for 12-hour clock") + end + + @hour = 0 if @hour == 12 + end + end if unix_seconds = @unix_seconds return Time.unix(unix_seconds) @@ -203,18 +218,22 @@ struct Time::Format end def hour_24_zero_padded + @hour_is_12 = false @hour = consume_number(2) end def hour_24_blank_padded + @hour_is_12 = false @hour = consume_number_blank_padded(2) end def hour_12_zero_padded hour_24_zero_padded + @hour_is_12 = true end def hour_12_blank_padded + @hour_is_12 = true @hour = consume_number_blank_padded(2) end @@ -269,7 +288,7 @@ struct Time::Format string = consume_string case string.downcase when "am" - # skip + @pm = false when "pm" @pm = true else diff --git a/src/tuple.cr b/src/tuple.cr index 496f5a346e55..6288983dd81e 100644 --- a/src/tuple.cr +++ b/src/tuple.cr @@ -393,7 +393,7 @@ struct Tuple # ``` def to_s(io : IO) : Nil io << '{' - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << '}' end diff --git a/src/uri.cr b/src/uri.cr index 97e76e1b918d..2ccb8af26e17 100644 --- a/src/uri.cr +++ b/src/uri.cr @@ -446,7 +446,7 @@ class URI elsif dst_path.first.includes?(':') # (see RFC2396 Section 5) String.build do |io| io << "./" - dst_path.join('/', io) + dst_path.join(io, '/') end else string = dst_path.join('/') @@ -459,7 +459,7 @@ class URI else String.build do |io| base_path.size.times { io << "../" } - dst_path.join('/', io) + dst_path.join(io, '/') end end end diff --git a/src/uri/encoding.cr b/src/uri/encoding.cr index f04c90450da7..538ae0f85129 100644 --- a/src/uri/encoding.cr +++ b/src/uri/encoding.cr @@ -222,7 +222,7 @@ class URI else io.write_byte '%'.ord.to_u8 io.write_byte '0'.ord.to_u8 if byte < 16 - byte.to_s(16, io, upcase: true) + byte.to_s(io, 16, upcase: true) end end io diff --git a/src/uuid/json.cr b/src/uuid/json.cr index 062787985db8..edc38182c0db 100644 --- a/src/uuid/json.cr +++ b/src/uuid/json.cr @@ -12,7 +12,9 @@ struct UUID # require "uuid/json" # # class Example - # JSON.mapping id: UUID + # include JSON::Serializable + # + # property id : UUID # end # # example = Example.from_json(%({"id": "ba714f86-cac6-42c7-8956-bcf5105e1b81"})) diff --git a/src/va_list.cr b/src/va_list.cr index eb0d0121f612..b24304c32e4c 100644 --- a/src/va_list.cr +++ b/src/va_list.cr @@ -18,7 +18,17 @@ struct VaList end end - {% if compare_versions(Crystal::VERSION, "0.33.0-0") > 0 %} + {% if flag?(:aarch64) || flag?(:win32) %} + {% platform = flag?(:aarch64) ? "AArch64" : "Windows" %} + {% clang_impl = flag?(:aarch64) ? "https://github.com/llvm/llvm-project/blob/a574edbba2b24fcfb733aa2d82308131f5b7d2d6/clang/lib/CodeGen/TargetInfo.cpp#L5677-L5921" : "https://github.com/llvm/llvm-project/blob/a574edbba2b24fcfb733aa2d82308131f5b7d2d6/clang/lib/CodeGen/TargetInfo.cpp#L5958-L5964" %} + # Do not call this, instead use C wrappers calling the va_arg macro for the types you need. + # + # Clang implements va_arg on {{platform.id}} like this: {{clang_impl.id}} + # If somebody wants to fix the LLVM IR va_arg instruction on {{platform}} upstream, or port the above here, that would be welcome. + def next(type) + \{% raise "Cannot get variadic argument on {{platform.id}}. As a workaround implement wrappers in C calling the va_arg macro for the types you need and bind to those." %} + end + {% else %} @[Primitive(:va_arg)] def next(type) end diff --git a/src/xml/attributes.cr b/src/xml/attributes.cr index 146829aeea93..07226f1e46b2 100644 --- a/src/xml/attributes.cr +++ b/src/xml/attributes.cr @@ -60,7 +60,7 @@ struct XML::Attributes def to_s(io : IO) : Nil io << '[' - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << ']' end diff --git a/src/xml/builder.cr b/src/xml/builder.cr index ba8716a920bf..4597314ed99d 100644 --- a/src/xml/builder.cr +++ b/src/xml/builder.cr @@ -5,10 +5,13 @@ # without a matching `start_element`, or trying to use # a non-string value as an object's field name) struct XML::Builder + private CDATA_END = "]]>" + private CDATA_ESCAPE = "]]]]>" + @box : Void* # Creates a builder that writes to the given *io*. - def initialize(io : IO) + def initialize(@io : IO) @box = Box.box(io) buffer = LibXML.xmlOutputBufferCreateIO( ->(ctx, buffer, len) { @@ -180,14 +183,17 @@ struct XML::Builder # Emits the start of a `CDATA` section, invokes the block # and then emits the end of the `CDATA` section. - def cdata + # + # NOTE: `CDATA` end sequences written within the block + # need to be escaped manually. + def cdata(&) start_cdata yield.tap { end_cdata } end - # Emits a `CDATA` section. + # Emits a `CDATA` section. Escapes nested `CDATA` end sequences. def cdata(text : String) : Nil - call WriteCDATA, string_to_unsafe(text) + call WriteCDATA, string_to_unsafe(text.gsub(CDATA_END, CDATA_ESCAPE)) end # Emits the start of a comment. @@ -243,6 +249,8 @@ struct XML::Builder # this writer's `IO`. def flush call Flush + + @io.flush end # Sets the indent string. @@ -370,7 +378,6 @@ module XML # when StartDocument is omitted. xml.end_document xml.flush - io.flush v end end diff --git a/src/xml/libxml2.cr b/src/xml/libxml2.cr index c348eec4a151..63657b2ae000 100644 --- a/src/xml/libxml2.cr +++ b/src/xml/libxml2.cr @@ -4,7 +4,11 @@ require "./parser_options" require "./html_parser_options" require "./save_options" -@[Link("xml2")] +{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %} + @[Link("xml2", pkg_config: "libxml-2.0")] +{% else %} + @[Link("xml2")] +{% end %} lib LibXML alias Int = LibC::Int diff --git a/src/xml/namespace.cr b/src/xml/namespace.cr index 8f977dd5817a..5bcd22995fa8 100644 --- a/src/xml/namespace.cr +++ b/src/xml/namespace.cr @@ -21,7 +21,7 @@ struct XML::Namespace def to_s(io : IO) : Nil io << "# String? + each_namespace do |namespace| + prefix = namespace.prefix ? "xmlns:#{namespace.prefix}" : "xmlns" + namespaces[prefix] = namespace.href + end + namespaces + end + protected def each_namespace(& : Namespace ->) ns_list = LibXML.xmlGetNsList(@node.value.doc, @node) if ns_list while ns_list.value - namespace = Namespace.new(document, ns_list.value) - prefix = namespace.prefix - namespaces[prefix ? "xmlns:#{prefix}" : "xmlns"] = namespace.href + yield Namespace.new(document, ns_list.value) ns_list += 1 end end - - namespaces end # Returns the address of underlying `LibXML::Node*` in memory. @@ -483,7 +486,15 @@ struct XML::Node # Raises `XML::Error` on evaluation error. def xpath(path, namespaces = nil, variables = nil) ctx = XPathContext.new(self) - ctx.register_namespaces namespaces if namespaces + + if namespaces + ctx.register_namespaces namespaces + else + root.try &.each_namespace do |namespace| + ctx.register_namespace namespace.prefix || "xmlns", namespace.href + end + end + ctx.register_variables variables if variables ctx.evaluate(path) end @@ -569,19 +580,9 @@ struct XML::Node ptr ? (ptr.as(Array(XML::Error))) : nil end - private SUBSTITUTIONS = { - '>' => ">", - '<' => "<", - '"' => """, - '\'' => "'", - '&' => "&", - } - - private def escape(string) - if string.includes? '\0' + private def check_no_null_byte(string) + if string.includes? Char::ZERO raise XML::Error.new("Cannot escape string containing null character", 0) end - - string.gsub(SUBSTITUTIONS) end end diff --git a/src/xml/node_set.cr b/src/xml/node_set.cr index 8c2e687ec758..541a440d1f09 100644 --- a/src/xml/node_set.cr +++ b/src/xml/node_set.cr @@ -33,7 +33,7 @@ struct XML::NodeSet def inspect(io : IO) : Nil io << '[' - join ", ", io, &.inspect(io) + join io, ", ", &.inspect(io) io << ']' end diff --git a/src/xml/xpath_context.cr b/src/xml/xpath_context.cr index 25af8e92683d..f6509237e023 100644 --- a/src/xml/xpath_context.cr +++ b/src/xml/xpath_context.cr @@ -41,8 +41,9 @@ struct XML::XPathContext end end - def register_namespace(prefix, uri) - LibXML.xmlXPathRegisterNs(self, prefix.to_s, uri.to_s) + def register_namespace(prefix : String, uri : String?) + prefix = prefix.lchop("xmlns:") + LibXML.xmlXPathRegisterNs(self, prefix, uri.to_s) end def register_variables(variables) diff --git a/src/yaml.cr b/src/yaml.cr index b238b5b52a17..cc7f0251b364 100644 --- a/src/yaml.cr +++ b/src/yaml.cr @@ -50,7 +50,7 @@ require "base64" # anchored values (see `YAML::PullParser` for an explanation of this). # # Crystal primitive types, `Time`, `Bytes` and `Union` implement -# this method. `YAML.mapping` can be used to implement this method +# this method. `YAML::Serializable` can be used to implement this method # for user types. # # ### Dumping with `YAML.dump` or `#to_yaml` @@ -64,7 +64,7 @@ require "base64" # `to_yaml(builder : YAML::Nodes::Builder`). # # Crystal primitive types, `Time` and `Bytes` implement -# this method. `YAML.mapping` can be used to implement this method +# this method. `YAML::Serializable` can be used to implement this method # for user types. # # ``` diff --git a/src/yaml/any.cr b/src/yaml/any.cr index 300e1e59cb69..e037afa2ceb7 100644 --- a/src/yaml/any.cr +++ b/src/yaml/any.cr @@ -128,14 +128,12 @@ struct YAML::Any # Traverses the depth of a structure and returns the value. # Returns `nil` if not found. - def dig?(index_or_key, *subkeys) - if value = self[index_or_key]? - value.dig?(*subkeys) - end + def dig?(index_or_key, *subkeys) : YAML::Any? + self[index_or_key]?.try &.dig?(*subkeys) end # :nodoc: - def dig?(index_or_key) + def dig?(index_or_key) : YAML::Any? case @raw when Hash, Array self[index_or_key]? @@ -145,15 +143,12 @@ struct YAML::Any end # Traverses the depth of a structure and returns the value, otherwise raises. - def dig(index_or_key, *subkeys) - if (value = self[index_or_key]) && value.responds_to?(:dig) - return value.dig(*subkeys) - end - raise "YAML::Any value not diggable for key: #{index_or_key.inspect}" + def dig(index_or_key, *subkeys) : YAML::Any + self[index_or_key].dig(*subkeys) end # :nodoc: - def dig(index_or_key) + def dig(index_or_key) : YAML::Any self[index_or_key] end diff --git a/src/yaml/builder.cr b/src/yaml/builder.cr index c93cb8817091..03e5a86a46c7 100644 --- a/src/yaml/builder.cr +++ b/src/yaml/builder.cr @@ -26,7 +26,7 @@ class YAML::Builder @box : Void* - # By default the maximum nesting of sequences/amppings is 99. Nesting more + # By default the maximum nesting of sequences/mappings is 99. Nesting more # than this will result in a YAML::Error. Changing the value of this property # allows more/less nesting. property max_nesting = 99 @@ -183,6 +183,8 @@ class YAML::Builder # Flushes any pending data to the underlying `IO`. def flush LibYAML.yaml_emitter_flush(@emitter) + + @io.flush end def finalize diff --git a/src/yaml/from_yaml.cr b/src/yaml/from_yaml.cr index da46d14aea24..12fa47ba64b2 100644 --- a/src/yaml/from_yaml.cr +++ b/src/yaml/from_yaml.cr @@ -242,7 +242,7 @@ def Union.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) {% end %} {% end %} - node.raise("Error deserailizing alias") + node.raise("Error deserializing alias") end {% begin %} diff --git a/src/yaml/lib_yaml.cr b/src/yaml/lib_yaml.cr index f766ff6f1dd5..9faa3abf339e 100644 --- a/src/yaml/lib_yaml.cr +++ b/src/yaml/lib_yaml.cr @@ -1,6 +1,10 @@ require "./enums" -@[Link("yaml")] +{% if compare_versions(Crystal::VERSION, "0.35.0-0") >= 0 %} + @[Link("yaml", pkg_config: "yaml-0.1")] +{% else %} + @[Link("yaml")] +{% end %} lib LibYAML alias Int = LibC::Int diff --git a/src/yaml/mapping.cr b/src/yaml/mapping.cr deleted file mode 100644 index 74665ffb997f..000000000000 --- a/src/yaml/mapping.cr +++ /dev/null @@ -1,223 +0,0 @@ -module YAML - # The `YAML.mapping` macro defines how an object is mapped to YAML. - # - # It takes named arguments, a named tuple literal or a hash literal as argument, - # in which attributes and types are defined. - # Once defined, `Object#from_yaml` populates properties of the class from the - # YAML document. - # - # ``` - # require "yaml" - # - # class Employee - # YAML.mapping( - # title: String, - # name: String, - # ) - # end - # - # employee = Employee.from_yaml("title: Manager\nname: John") - # employee.title # => "Manager" - # employee.name # => "John" - # - # employee.name = "Jenny" - # employee.name # => "Jenny" - # ``` - # - # Attributes not mapped with `YAML.mapping` are not defined as properties. - # Also, missing attributes raise a `ParseException`. - # - # ``` - # employee = Employee.from_yaml("title: Manager\nname: John\nage: 30") - # employee.age # undefined method 'age'. (compile error) - # - # Employee.from_yaml("title: Manager") # raises YAML::ParseException - # ``` - # - # You can also define attributes for each property. - # - # ``` - # class Employer - # YAML.mapping( - # title: String, - # name: { - # type: String, - # nilable: true, - # key: "firstname", - # }, - # ) - # end - # ``` - # - # Available attributes: - # - # * *type* (required) defines its type. In the example above, *title: String* is a shortcut to *title: {type: String}*. - # * *nilable* defines if a property can be a `Nil`. Passing `T?` as a type has the same effect. - # * **default**: value to use if the property is missing in the YAML document, or if it's `null` and `nilable` was not set to `true`. If the default value creates a new instance of an object (for example `[1, 2, 3]` or `SomeObject.new`), a different instance will be used each time a YAML document is parsed. - # * *key* defines which key to read from a YAML document. It defaults to the name of the property. - # * *converter* takes an alternate type for parsing. It requires a `#from_yaml` method in that class, and returns an instance of the given type. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`. - # * **setter**: if `true`, will generate a setter for the variable, `true` by default - # * **getter**: if `true`, will generate a getter for the variable, `true` by default - # * **presence**: if `true`, a `{{key}}_present?` method will be generated when the key was present (even if it has a `null` value), `false` by default - # - # This macro by default defines getters and setters for each variable (this can be overrided with *setter* and *getter*). - # The mapping doesn't define a constructor accepting these variables as arguments, but you can provide an overload. - # - # The macro basically defines a constructor accepting a `YAML::PullParser` that reads from - # it and initializes this type's instance variables. - # - # This macro also declares instance variables of the types given in the mapping. - macro mapping(_properties_, strict = false) - {% for key, value in _properties_ %} - {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} - {% end %} - - {% for key, value in _properties_ %} - {% _properties_[key][:key_id] = key.id.gsub(/\?$/, "") %} - {% end %} - - {% for key, value in _properties_ %} - @{{value[:key_id]}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }} - - {% if value[:setter] == nil ? true : value[:setter] %} - def {{value[:key_id]}}=(_{{value[:key_id]}} : {{value[:type]}} {{ (value[:nilable] ? "?" : "").id }}) - @{{value[:key_id]}} = _{{value[:key_id]}} - end - {% end %} - - {% if value[:getter] == nil ? true : value[:getter] %} - def {{key.id}} - @{{value[:key_id]}} - end - {% end %} - - {% if value[:presence] %} - @{{value[:key_id]}}_present : Bool = false - - def {{value[:key_id]}}_present? - @{{value[:key_id]}}_present - end - {% end %} - {% end %} - - def self.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) - ctx.read_alias(node, \{{@type}}) do |obj| - return obj - end - - instance = allocate - - ctx.record_anchor(node, instance) - - instance.initialize(ctx, node, nil) - GC.add_finalizer(instance) if instance.responds_to?(:finalize) - instance - end - - # `new` and `initialize` with just `pull` as an argument collide - # and the compiler just sees the last one. This is why we add a - # dummy argument. - # - # FIXME: remove the dummy argument if we ever fix this. - - def initialize(ctx : YAML::ParseContext, node : ::YAML::Nodes::Node, _dummy : Nil) - {% for key, value in _properties_ %} - %var{key.id} = nil - %found{key.id} = false - {% end %} - - case node - when YAML::Nodes::Mapping - YAML::Schema::Core.each(node) do |key_node, value_node| - unless key_node.is_a?(YAML::Nodes::Scalar) - key_node.raise "Expected scalar as key for mapping" - end - - key = key_node.value - - case key - {% for key, value in _properties_ %} - when {{value[:key] || value[:key_id].stringify}} - %found{key.id} = true - - %var{key.id} = - {% if value[:nilable] || value[:default] != nil %} YAML::Schema::Core.parse_null_or(value_node) { {% end %} - - {% if value[:converter] %} - {{value[:converter]}}.from_yaml(ctx, value_node) - {% elsif value[:type].is_a?(Path) || value[:type].is_a?(Generic) %} - {{value[:type]}}.new(ctx, value_node) - {% else %} - ::Union({{value[:type]}}).new(ctx, value_node) - {% end %} - - {% if value[:nilable] || value[:default] != nil %} } {% end %} - {% end %} - else - {% if strict %} - key_node.raise "Unknown yaml attribute: #{key}" - {% end %} - end - end - when YAML::Nodes::Scalar - if node.value.empty? && node.style.plain? && !node.tag - # We consider an empty scalar as an empty mapping - else - node.raise "Expected mapping, not #{node.class}" - end - else - node.raise "Expected mapping, not #{node.class}" - end - - {% for key, value in _properties_ %} - {% unless value[:nilable] || value[:default] != nil %} - if %var{key.id}.nil? && !%found{key.id} && !::Union({{value[:type]}}).nilable? - node.raise "Missing yaml attribute: {{(value[:key] || value[:key_id]).id}}" - end - {% end %} - - {% if value[:nilable] %} - {% if value[:default] != nil %} - @{{value[:key_id]}} = %found{key.id} ? %var{key.id} : {{value[:default]}} - {% else %} - @{{value[:key_id]}} = %var{key.id} - {% end %} - {% elsif value[:default] != nil %} - @{{value[:key_id]}} = %var{key.id}.nil? ? {{value[:default]}} : %var{key.id} - {% else %} - @{{value[:key_id]}} = %var{key.id}.as({{value[:type]}}) - {% end %} - - {% if value[:presence] %} - @{{value[:key_id]}}_present = %found{key.id} - {% end %} - {% end %} - end - - def to_yaml(%yaml : ::YAML::Nodes::Builder) - %yaml.mapping(reference: self) do - {% for key, value in _properties_ %} - _{{value[:key_id]}} = @{{value[:key_id]}} - - unless _{{value[:key_id]}}.nil? - # Key - {{value[:key] || value[:key_id].stringify}}.to_yaml(%yaml) - - # Value - {% if value[:converter] %} - {{ value[:converter] }}.to_yaml(_{{value[:key_id]}}, %yaml) - {% else %} - _{{value[:key_id]}}.to_yaml(%yaml) - {% end %} - end - {% end %} - end - end - end - - # This is a convenience method to allow invoking `YAML.mapping` - # with named arguments instead of with a hash/named-tuple literal. - macro mapping(**_properties_) - ::YAML.mapping({{_properties_}}) - end -end diff --git a/src/yaml/serialization.cr b/src/yaml/serialization.cr index c0d3b23e7d26..10b0ae2b8f1e 100644 --- a/src/yaml/serialization.cr +++ b/src/yaml/serialization.cr @@ -59,9 +59,9 @@ module YAML # ``` # # `YAML::Field` properties: - # * **ignore**: if `true` skip this field in seriazation and deserialization (by default false) + # * **ignore**: if `true` skip this field in serialization and deserialization (by default false) # * **key**: the value of the key in the yaml object (by default the name of the instance variable) - # * **converter**: specify an alternate type for parsing and generation. The converter must define `from_yaml(YAML::PullParser)` and `to_yaml(value, YAML::Builder)` as class methods. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`. + # * **converter**: specify an alternate type for parsing and generation. The converter must define `from_yaml(YAML::ParseContext, YAML::Nodes::Node)` and `to_yaml(value, YAML::Nodes::Builder)` as class methods. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`. # * **presence**: if `true`, a `@{{key}}_present` instance variable will be generated when the key was present (even if it has a `null` value), `false` by default # * **emit_null**: if `true`, emits a `null` value for nilable property (by default nulls are not emitted) # diff --git a/src/yaml/to_yaml.cr b/src/yaml/to_yaml.cr index adbd75c0b86e..a6ef21a00cef 100644 --- a/src/yaml/to_yaml.cr +++ b/src/yaml/to_yaml.cr @@ -80,6 +80,21 @@ struct Number end end +struct Float + def to_yaml(yaml : YAML::Nodes::Builder) + infinite = self.infinite? + if infinite == 1 + yaml.scalar(".inf") + elsif infinite == -1 + yaml.scalar("-.inf") + elsif nan? + yaml.scalar(".nan") + else + yaml.scalar self.to_s + end + end +end + struct Nil def to_yaml(yaml : YAML::Nodes::Builder) yaml.scalar "" @@ -136,16 +151,17 @@ module Time::EpochMillisConverter end end -# Converter to be used with `YAML.mapping` +# Converter to be used with `YAML::Serializable` # to serialize the `Array(T)` elements with the custom converter. # # ``` # require "yaml" # # class Timestamp -# YAML.mapping({ -# values: {type: Array(Time), converter: YAML::ArrayConverter(Time::EpochConverter)}, -# }) +# include YAML::Serializable +# +# @[YAML::Field(converter: YAML::ArrayConverter(Time::EpochConverter))] +# values : Array(Time) # end # # timestamp = Timestamp.from_yaml(%({"values":[1459859781,1567628762]})) diff --git a/src/zip.cr b/src/zip.cr deleted file mode 100644 index a8c3459f3fba..000000000000 --- a/src/zip.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "compress/zip" - -{% puts "Warning: Zip is deprecated, use Compress::Zip" %} - -# DEPRECATED: Use `Compress::Zip` -alias Zip = Compress::Zip diff --git a/src/zlib.cr b/src/zlib.cr deleted file mode 100644 index d8e10e0c58fe..000000000000 --- a/src/zlib.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "compress/zlib" - -{% puts "Warning: Zlib is deprecated, use Compress::Zlib" %} - -# DEPRECATED: Use `Compress::Zlib` -alias Zlib = Compress::Zlib