diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..0d22929656 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# Keep the build context small. .git is intentionally NOT ignored: rbs.gemspec +# builds its file list with `git ls-files`. +# +# Safe to ignore here: untracked/generated paths, and tracked paths the gemspec +# already rejects (so `gem build` does not expect them on disk). Do NOT ignore a +# tracked path the gemspec ships — `gem build` would fail with "... are not +# files". .vscode/ and benchmark/ are tracked but rejected by the gemspec. +.vscode/ +benchmark/ + +# Editor / tooling / worktree copies (untracked; each is a full repo clone) +.claude/ +trees/ +.devcontainer/ +.idea/ +.ruby-lsp/ +.yardoc/ +doc/ +datasets/ +tmp/ + +# Build artifact — rebuilt inside the image by `rake wasm:jruby_setup` +lib/rbs/wasm/*.wasm +*.gem +ext/**/*.o +ext/**/*.so +ext/**/*.bundle +*.log + +# Local scratch / experiment files +flamegraph.html +gc.html +foo.rbs +sorted.json +string.json +Gemfile-* +gemfiles/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 748216d506..b7f8701fdf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,4 +21,4 @@ updates: schedule: interval: 'weekly' labels: - - 'no-milestone' + - 'skip-changelog' diff --git a/.github/workflows/bundle-update.yml b/.github/workflows/bundle-update.yml index 5639eb249f..fb14768620 100644 --- a/.github/workflows/bundle-update.yml +++ b/.github/workflows/bundle-update.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -58,6 +58,6 @@ jobs: --body "Automated weekly bundle update" \ --head "$(git rev-parse --abbrev-ref HEAD)" \ --base "${{ github.event.repository.default_branch }}" \ - --label "no-milestone" + --label "skip-changelog" gh pr merge --auto --merge "$(git rev-parse --abbrev-ref HEAD)" diff --git a/.github/workflows/c-check.yml b/.github/workflows/c-check.yml index d9ed0df5c4..43745ea35e 100644 --- a/.github/workflows/c-check.yml +++ b/.github/workflows/c-check.yml @@ -7,11 +7,14 @@ on: pull_request: {} merge_group: {} +permissions: + contents: read + jobs: format-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ruby/setup-ruby@v1 with: ruby-version: "4.0" @@ -24,8 +27,13 @@ jobs: sudo apt-get install -y libdb-dev curl autoconf automake m4 libtool - name: Install clang-format from LLVM run: | - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo apt-add-repository "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-20 main" + # The codename must match the `runs-on` image: `ubuntu-latest` is 24.04 (noble). + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo gpg --dearmor -o /usr/share/keyrings/llvm-archive-keyring.gpg + # Written directly rather than through `apt-add-repository`, which cannot + # parse `[signed-by=...]` inside a one-line `deb` shortcut. + echo "deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg] http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" \ + | sudo tee /etc/apt/sources.list.d/llvm.list sudo apt-get update sudo apt-get install -y clang-format-20 sudo ln -sf /usr/bin/clang-format-20 /usr/local/bin/clang-format @@ -35,9 +43,9 @@ jobs: - name: Install Re2c run: | cd /tmp - curl -L https://github.com/skvadrik/re2c/archive/refs/tags/4.3.tar.gz > re2c-4.3.tar.gz - tar xf re2c-4.3.tar.gz - cd re2c-4.3 + curl -L https://github.com/skvadrik/re2c/archive/refs/tags/4.5.1.tar.gz > re2c-4.5.1.tar.gz + tar xf re2c-4.5.1.tar.gz + cd re2c-4.5.1 cmake --preset=linux-gcc-release-ootree-skeleton-fast cmake --build --preset=linux-gcc-release-ootree-skeleton-fast --parallel="$(nproc)" sudo ln -sf "$(pwd)"/.build/linux-gcc-release-ootree-skeleton-fast/re2c /usr/local/bin/re2c diff --git a/.github/workflows/comments.yml b/.github/workflows/comments.yml index fe0439b3af..8fd66e924f 100644 --- a/.github/workflows/comments.yml +++ b/.github/workflows/comments.yml @@ -13,10 +13,10 @@ jobs: # env: # RUBY_COMMIT: v4.0.0-preview2 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ruby/setup-ruby@v1 with: - ruby-version: "4.0.1" + ruby-version: "4.0.6" bundler: none - name: Install dependencies run: | diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 54dc9ff5c3..fe5d67a3ec 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -17,7 +17,7 @@ jobs: uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 id: metadata - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Abort if blocker files are changed diff --git a/.github/workflows/jruby.yml b/.github/workflows/jruby.yml new file mode 100644 index 0000000000..d898ed65e1 --- /dev/null +++ b/.github/workflows/jruby.yml @@ -0,0 +1,74 @@ +name: JRuby + +on: + push: + branches: + - master + pull_request: + paths: + - ".github/workflows/jruby.yml" + - "include/**" + - "src/**" + - "wasm/**" + - "lib/**" + - "sig/**" + - "core/**" + - "stdlib/**" + - "test/**" + - "Rakefile" + - "rbs.gemspec" + # `merge_group` has no `paths` filter, so this runs on every queue entry. + merge_group: {} + +permissions: + contents: read + +env: + # Keep in sync with .github/workflows/wasm.yml. + WASI_SDK_VERSION: "33" + WASI_SDK_RELEASE: "33.0" + +jobs: + test: + name: jruby + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + + # Build the .wasm and fetch the Chicory jars with CRuby + the WASI SDK, + # then run RBS itself on JRuby against those artifacts. + - name: Set up Ruby (to assemble the WebAssembly runtime) + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + - name: Install the WASI SDK + run: | + url="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_RELEASE}-x86_64-linux.tar.gz" + mkdir -p "$HOME/wasi-sdk" + curl -sSL "$url" | tar xz --strip-components=1 -C "$HOME/wasi-sdk" + echo "WASI_SDK_PATH=$HOME/wasi-sdk" >> "$GITHUB_ENV" + - name: Build rbs_parser.wasm + run: bundle exec rake wasm:jruby_setup + + - name: Set up JRuby + uses: ruby/setup-ruby@v1 + with: + ruby-version: jruby + bundler: none + - name: Install runtime and test gems + run: gem install prism rake rake-compiler test-unit rdoc rspec minitest json-schema pry --no-document + # jar-dependencies resolves through the JVM, so download the Chicory/ASM + # jars into ~/.m2 here on JRuby (jar-dependencies is not available in the + # CRuby step above). + - name: Download the Chicory and ASM jars + run: jruby -S rake wasm:install_jars + - name: Run the test suite on JRuby + run: jruby -S rake test diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml deleted file mode 100644 index f0dc278403..0000000000 --- a/.github/workflows/milestone.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Check milestone - -on: - pull_request: - types: [opened, edited, labeled, unlabeled, milestoned, demilestoned, synchronize] - -jobs: - check: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: Extract RBS::Version - id: version - run: | - # Extract version string from lib/rbs/version.rb - version=$(ruby -e 'load "lib/rbs/version.rb"; print RBS::VERSION') - echo "version=$version" >> "$GITHUB_OUTPUT" - - # Parse major.minor.patch - IFS='.' read -r major minor patch _rest <<< "$version" - echo "major=$major" >> "$GITHUB_OUTPUT" - echo "minor=$minor" >> "$GITHUB_OUTPUT" - echo "patch=$patch" >> "$GITHUB_OUTPUT" - - echo "RBS::VERSION = $version (major=$major, minor=$minor, patch=$patch)" - - - name: Check milestone - uses: actions/github-script@v8 - with: - script: | - const pr = context.payload.pull_request; - const milestone = pr.milestone; - const labels = pr.labels.map(l => l.name); - - const version = '${{ steps.version.outputs.version }}'; - const major = '${{ steps.version.outputs.major }}'; - const minor = '${{ steps.version.outputs.minor }}'; - const patch = parseInt('${{ steps.version.outputs.patch }}', 10); - - if (!milestone) { - if (labels.includes('no-milestone')) { - core.info('No milestone set, but no-milestone label is present — OK'); - return; - } - core.setFailed( - 'No milestone set. Add a milestone or add the "no-milestone" label.' - ); - return; - } - - if (labels.includes('no-milestone')) { - core.setFailed( - 'Milestone is set but "no-milestone" label is present. Remove the label or the milestone.' - ); - return; - } - - const milestoneName = milestone.title; - core.info(`Milestone: "${milestoneName}", RBS::VERSION: ${version}`); - - // Expected milestone based on version: - // patch == 0 → "RBS major.minor" (e.g. "RBS 4.0") - // patch >= 1 → "RBS major.minor.x" (e.g. "RBS 4.0.x") - let expectedMilestone; - if (patch === 0) { - expectedMilestone = `RBS ${major}.${minor}`; - } else { - expectedMilestone = `RBS ${major}.${minor}.x`; - } - - if (milestoneName !== expectedMilestone) { - core.setFailed( - `Milestone "${milestoneName}" does not match RBS::VERSION ${version}. ` + - `Expected milestone: "${expectedMilestone}"` - ); - } else { - core.info(`Milestone "${milestoneName}" matches RBS::VERSION ${version}`); - } diff --git a/.github/workflows/release-gems.yml b/.github/workflows/release-gems.yml new file mode 100644 index 0000000000..f4b813d1c7 --- /dev/null +++ b/.github/workflows/release-gems.yml @@ -0,0 +1,235 @@ +name: Release gems + +# Builds, publishes, and announces a release. Dispatch it with the commit being +# released and the version that commit declares. +# +# Everything is built from `commit`, not from wherever the default branch happens to +# be when the run starts, so the release describes a state that is already immutable. +# `version` is the same fact stated a second time -- the run stops before anything is +# built unless it matches the `RBS::VERSION` of that commit, so dispatching the wrong +# commit, or the right one under the wrong name, is a failed run rather than a gem +# that has to be yanked. +# +# `dry_run` builds and checks both gems and stops before the tag, which is how the +# build is exercised without releasing anything. +# +# | Gem | Platform | Parser | +# | -------------------- | ------------- | -------------------------------- | +# | `rbs-X.Y.Z.gem` | `ruby` (MRI) | C extension, compiled on install | +# | `rbs-X.Y.Z-java.gem` | `java` (JRuby)| `rbs_parser.wasm`, prebuilt here | +# +# See docs/release.md. The `java` gem is built on CRuby: the platform comes from +# `RBS_PLATFORM`, not from the engine running `gem build`. JRuby is only needed to +# run the result, which the workflow does before it would publish anything. +# +# One job on purpose. Tagging, pushing the gems, and opening the GitHub release +# all belong to a single release, and keeping them in one place keeps their order +# readable -- the tag is created once both gems are known to build and run, and +# before anything is published, so the reversible step comes before the irreversible +# one. +# +# The file name is what the RubyGems trusted publisher for `rbs` is registered +# against, so it cannot be renamed without registering the new name first. + +on: + workflow_dispatch: + inputs: + commit: + description: "Commit to release, as a full 40-character SHA" + required: true + version: + description: "Version to release, without the leading `v` (e.g. `4.1.2`)" + required: true + dry_run: + description: "Build and check the gems without tagging or publishing anything" + type: boolean + default: false + +permissions: + contents: read + +env: + # Keep in sync with .github/workflows/wasm.yml and .github/workflows/jruby.yml. + WASI_SDK_VERSION: "33" + WASI_SDK_RELEASE: "33.0" + +jobs: + release: + name: release + runs-on: ubuntu-latest + permissions: + contents: write # push the tag, publish the GitHub release + id-token: write # trusted publishing to RubyGems + env: + # The inputs are read through the environment rather than interpolated into + # the scripts below. + COMMIT: ${{ inputs.commit }} + VERSION: ${{ inputs.version }} + TAG: v${{ inputs.version }} + steps: + # The gemspec takes its file list from `git ls-files`, so both gems are built + # from the committed state -- of the dispatched commit, since that is what is + # checked out. The full history is needed to tell which branches contain it. + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.commit }} + fetch-depth: 0 + + # Before anything is installed or built: these are the two things the release + # is named after and built from, and a mistake in either is cheapest to catch + # here. + - name: Check the inputs + run: | + if [[ ! "$COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::\`$COMMIT\` is not a full 40-character SHA. A release names one exact commit." + exit 1 + fi + if [[ ! "$VERSION" =~ ^[0-9][0-9a-zA-Z.]*$ ]]; then + echo "::error::\`$VERSION\` is not a version number. Pass it without the leading \`v\`." + exit 1 + fi + + # A release proper is cut from the default branch, while a patch release can + # be cut from a release branch, so which branch the commit is on is not this + # workflow's business. That it is on one is: a commit no branch contains is + # one that nothing in the repository leads to any more. + git fetch --no-tags origin "+refs/heads/*:refs/remotes/origin/*" + branches=$(git branch --remotes --contains "$COMMIT" --format "%(refname:lstrip=3)") + if [ -z "$branches" ]; then + echo "::error::$COMMIT is not on any branch." + exit 1 + fi + echo "Branches containing $COMMIT:" + printf '%s\n' "$branches" + + # A tag that already exists is a version that has already been released, and + # pushing it would fail after the build rather than before it. + - name: Check that the tag does not exist + if: ${{ !inputs.dry_run }} + run: | + if git ls-remote --exit-code --tags origin "refs/tags/$TAG" > /dev/null; then + echo "::error::$TAG already exists, so $VERSION has been released." + exit 1 + fi + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + + # Fails before a minute is spent on the build, and before anything is pushed: + # the version has to be the one the released commit declares, and -- unless + # this is a `.dev.N` release -- the one CHANGELOG.md is written up for. + - name: Check the version and the changelog + run: bundle exec rake "gem:check_release[$VERSION]" + + - name: Build the ruby gem + run: | + mkdir -p pkg + gem build rbs.gemspec -o "pkg/rbs-$VERSION.gem" + + # `rake wasm:jruby_setup` compiles src/**/*.c to WebAssembly and copies the + # result to lib/rbs/wasm/, where the gemspec picks it up. clang runs as a + # subprocess, so this works on CRuby. + - name: Install the WASI SDK + run: | + url="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_RELEASE}-x86_64-linux.tar.gz" + mkdir -p "$HOME/wasi-sdk" + curl -sSL "$url" | tar xz --strip-components=1 -C "$HOME/wasi-sdk" + echo "WASI_SDK_PATH=$HOME/wasi-sdk" >> "$GITHUB_ENV" + - name: Build rbs_parser.wasm + run: bundle exec rake wasm:jruby_setup + + - name: Build the java gem + env: + RBS_PLATFORM: java + run: gem build rbs.gemspec -o "pkg/rbs-$VERSION-java.gem" + + # `git ls-files` vouches for everything else, but rbs_parser.wasm is a build + # artifact, so the java gem is the one that can come out quietly wrong. + - name: Check the built gems + run: | + ruby -rrubygems/package -e ' + ruby_gem, java_gem = ARGV.map { Gem::Package.new(_1).spec } + + raise "unexpected platform: #{ruby_gem.platform}" unless ruby_gem.platform.to_s == "ruby" + raise "the C extension is not declared" if ruby_gem.extensions.empty? + + raise "unexpected platform: #{java_gem.platform}" unless java_gem.platform.to_s == "java" + raise "rbs_parser.wasm is missing" unless java_gem.files.include?("lib/rbs/wasm/rbs_parser.wasm") + raise "the java gem must not declare an extension" unless java_gem.extensions.empty? + + [ruby_gem, java_gem].each { puts "#{_1.full_name}: #{_1.files.size} files" } + ' "pkg/rbs-$VERSION.gem" "pkg/rbs-$VERSION-java.gem" + + # The checks above cannot tell whether rbs_parser.wasm actually runs. Install + # the gem the way a user would -- jar-dependencies fetches Chicory and ASM + # from Maven during the install -- and parse something with it, so the + # WebAssembly runtime is exercised end to end before anything is published. + - name: Set up JRuby + uses: ruby/setup-ruby@v1 + with: + ruby-version: jruby + bundler: none + - name: Check the java gem on JRuby + run: | + gem install "pkg/rbs-$VERSION-java.gem" + ruby -e ' + require "rbs" + _, _, decls = RBS::Parser.parse_signature("class Foo end") + names = decls.map { _1.name.to_s } + raise "parsed #{names.inspect}, expected [\"Foo\"]" unless names == ["Foo"] + puts "#{RUBY_ENGINE} #{RUBY_VERSION}: rbs #{RBS::VERSION} parses through the WebAssembly runtime" + ' + + - name: Switch back to CRuby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + + # Uploaded before publishing, so a failed push still leaves the gems behind. + # This is also where a dry run ends. + - uses: actions/upload-artifact@v7 + with: + name: gems + path: pkg/*.gem + if-no-files-found: error + + # Everything below runs only for a real release. + + # The tag comes after the gems are known to build and run, and before anything + # is published: a tag can be deleted, while a version pushed to RubyGems can + # only be yanked. What it names was decided by the checkout rather than by the + # tagging, so nothing rests on it being created first. + - name: Tag the release + if: ${{ !inputs.dry_run }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + bundle exec rake gem:tag + + - name: Configure RubyGems credentials + if: ${{ !inputs.dry_run }} + # No floating major tag on this action, so the exact release is pinned. + uses: rubygems/configure-rubygems-credentials@v2.1.0 + + - name: Push the gems + if: ${{ !inputs.dry_run }} + run: | + gem push "pkg/rbs-$VERSION.gem" + gem push "pkg/rbs-$VERSION-java.gem" + + # Last, so that a failed push never announces a release that has no gems. + - name: Publish the GitHub release + if: ${{ !inputs.dry_run }} + env: + GH_TOKEN: ${{ github.token }} + run: bundle exec rake gem:gh_release diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 56d9ab8ec7..958bdcb3f7 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -7,13 +7,16 @@ on: pull_request: {} merge_group: {} +permissions: + contents: read + jobs: test: runs-on: "ubuntu-latest" strategy: fail-fast: false matrix: - ruby: ['3.2', '3.3', '3.4', '4.0', head] + ruby: ['3.3', '3.4', '4.0', head] rubyopt: [""] job: - test @@ -37,7 +40,7 @@ jobs: env: RANDOMIZE_STDLIB_TEST_ORDER: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} @@ -87,7 +90,7 @@ jobs: matrix: ruby: ['4.0', head] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install dependencies run: | brew install ruby-build @@ -124,7 +127,7 @@ jobs: matrix: ruby: ['4.0', head] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install dependencies run: | brew install ruby-build diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3df481c3ff..37ad5c79cc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,10 +8,15 @@ on: paths: - ".github/workflows/rust.yml" - "rust/**" + # `merge_group` has no `paths` filter, so this runs on every queue entry. + merge_group: {} env: RUSTFLAGS: "-D warnings" +permissions: + contents: read + jobs: test: name: cargo:test @@ -21,7 +26,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -40,7 +45,7 @@ jobs: run: | rustup update --no-self-update stable rustup default stable - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: | ~/.cargo/registry @@ -59,7 +64,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up git identity run: | @@ -82,7 +87,7 @@ jobs: run: | rustup update --no-self-update stable rustup default stable - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: | ~/.cargo/registry @@ -101,7 +106,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up git identity run: | @@ -124,7 +129,7 @@ jobs: run: | rustup update --no-self-update stable rustup default stable - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: | ~/.cargo/registry @@ -142,7 +147,7 @@ jobs: name: cargo:lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -162,7 +167,7 @@ jobs: rustup update --no-self-update stable rustup default stable rustup component add --toolchain stable clippy rustfmt - - uses: actions/cache@v5 + - uses: actions/cache@v6 with: path: | ~/.cargo/registry diff --git a/.github/workflows/truffleruby.yml b/.github/workflows/truffleruby.yml new file mode 100644 index 0000000000..d58de2f584 --- /dev/null +++ b/.github/workflows/truffleruby.yml @@ -0,0 +1,54 @@ +name: TruffleRuby + +on: + push: + branches: + - master + pull_request: {} + merge_group: {} + +permissions: + contents: read + +jobs: + test: + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + ruby: ['truffleruby'] + job: + - test + env: + RANDOMIZE_STDLIB_TEST_ORDER: "true" + # TruffleRuby warns and falls back to US-ASCII unless the locale is UTF-8. + LANG: "en_US.UTF-8" + steps: + - uses: actions/checkout@v7 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler: none + - name: Set working directory as safe + run: git config --global --add safe.directory $(pwd) + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libdb-dev curl autoconf automake m4 libtool python3 + - name: Update rubygems & bundler + run: | + ruby -v + gem update --system + - name: install erb + run: gem install erb + - name: bundle config + run: | + # Profilers and the typecheck tooling are not needed for the smoke test, + # and some of them do not support TruffleRuby. + bundle config set --local without 'profilers typecheck_test' + - name: bin/setup + run: | + bin/setup + - name: Run test + run: | + bundle exec rake ${{ matrix.job }} diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 109becfd14..f2327abb1d 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -7,16 +7,19 @@ on: pull_request: {} merge_group: {} +permissions: + contents: read + jobs: steep: runs-on: "ubuntu-latest" strategy: fail-fast: false steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: ruby/setup-ruby@v1 with: - ruby-version: "3.4" + ruby-version: "4.0" bundler: none - name: Set working directory as safe run: git config --global --add safe.directory $(pwd) diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 0000000000..0f3a6c072b --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,55 @@ +name: WebAssembly + +on: + push: + branches: + - master + pull_request: + paths: + - ".github/workflows/wasm.yml" + - "Rakefile" + - "include/**" + - "src/**" + - "wasm/**" + # `merge_group` has no `paths` filter, so this runs on every queue entry. + merge_group: {} + +permissions: + contents: read + +env: + # Pinned so the smoke test is reproducible. Bump together when upgrading. + WASI_SDK_VERSION: "33" + WASI_SDK_RELEASE: "33.0" + WASMTIME_VERSION: "v47.0.2" + +jobs: + build: + name: wasm:check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + - name: Install the WASI SDK + run: | + url="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_RELEASE}-x86_64-linux.tar.gz" + mkdir -p "$HOME/wasi-sdk" + curl -sSL "$url" | tar xz --strip-components=1 -C "$HOME/wasi-sdk" + echo "WASI_SDK_PATH=$HOME/wasi-sdk" >> "$GITHUB_ENV" + - name: Install wasmtime + uses: bytecodealliance/actions/wasmtime/setup@v1 + with: + version: ${{ env.WASMTIME_VERSION }} + - name: Build and smoke-test the WebAssembly module + run: bundle exec rake wasm:check diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 1733e7d5e8..dc4dfa05d4 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,15 +7,18 @@ on: pull_request: {} merge_group: {} +permissions: + contents: read + jobs: compile: runs-on: "windows-latest" strategy: fail-fast: false matrix: - ruby: ['3.4', ucrt, mswin] + ruby: ['4.0', ucrt, mswin] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: load ruby uses: ruby/setup-ruby@v1 with: diff --git a/.gitignore b/.gitignore index 3d723e6d4b..9ab87e2db3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,11 @@ ext/rbs_extension/.cache # Rust crate vendored RBS source (managed by rake rust:rbs:sync or rust:rbs:symlink) rust/ruby-rbs-sys/vendor/rbs/ rust/ruby-rbs/vendor/rbs/ + +# Compiled WebAssembly module (built by rake wasm:build) +wasm/*.wasm + +# JRuby runtime artifact (built by rake wasm:jruby_setup, bundled in the -java +# gem). The require_jar file lib/rbs_jars.rb is committed; the Chicory/ASM jars +# themselves live in ~/.m2 (jar-dependencies), not here. +lib/rbs/wasm/*.wasm diff --git a/CHANGELOG.md b/CHANGELOG.md index 046f71aac0..7a6923c174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,119 @@ # CHANGELOG +## 4.1.2 (2026-08-03) + +### Miscellaneous + +* Keep GC disabled during GC_test#test_stress_and_stress= ([#3061](https://github.com/ruby/rbs/pull/3061)) +* Fix GC_test#test_enable leaving GC disabled for the rest of the suite ([#3059](https://github.com/ruby/rbs/pull/3059)) + +## 4.1.1 (2026-07-30) + +### Library changes + +* Include the trailing `?` in a keyword key's symbol location ([#3043](https://github.com/ruby/rbs/pull/3043)) +* Mark `type_name` as optional on the alias annotations ([#3039](https://github.com/ruby/rbs/pull/3039)) + +### Miscellaneous + +* Fix template drift for ext/rbs_extension/ast_translation.c ([#3038](https://github.com/ruby/rbs/pull/3038)) + +## 4.1.0 (2026-07-27) + +RBS 4.1 ships with JRuby support. The RBS parser is written in plain C without depending on the Ruby C API, so it is compiled to WebAssembly and runs on a Wasm runtime, with the parsed AST serialized in a binary format and decoded into RBS objects. The full test suite runs on JRuby in CI. + +The inline RBS syntax gets three new features: singleton method definitions (`def self.`), the `module-self` constraint, and instance variable annotations in `module` declarations. Note that RBS inline is still experimental and may change in future releases. + +This release also introduces `RBS::Rewriter`, an API to edit RBS source text while preserving the surrounding content, which `rbs annotate` is now built on. Parsing performance is improved by interning type names in a shared trie and reducing allocations per node. + +`RBS::Prototype::Runtime`, the RDoc plugin parser, and the top-level `float` type alias are deprecated in this release. + +### Signature updates + +**Updated classes/modules/methods:** `ARGF`, `Array`, `CSV`, `Class`, `Delegator`, `Digest`, `ERB`, `Enumerable`, `Enumerator`, `Enumerator::Product`, `Etc`, `File`, `File::Constants`, `File::Stat`, `FileUtils`, `Float`, `Gem`, `Hash`, `IO`, `IPAddr`, `Integer`, `JSON`, `Kernel`, `MatchData`, `Module`, `Monitor`, `Numeric`, `ObjectSpace::WeakKeyMap`, `OpenSSL`, `Pathname`, `RBS::Ops`, `Ractor`, `Range`, `Resolv`, `RubyVM::InstructionSequence`, `Set`, `Shellwords`, `String`, `StringIO`, `StringScanner`, `Struct`, `Tempfile`, `Thread`, `Timeout`, `TSort`, `URI::Generic`, `Zlib::GzipReader`, `Zlib::ZStream` + +* Remove stale Float constants and add a constant drift test ([#2994](https://github.com/ruby/rbs/pull/2994)) +* Update RDoc comments with Ruby 4.0.6 ([#3035](https://github.com/ruby/rbs/pull/3035)) +* Add a dependency on tempfile. ([#3025](https://github.com/ruby/rbs/pull/3025)) +* Support `%a{implicitly-returns-nil}` on `MatchData#[]` ([#2990](https://github.com/ruby/rbs/pull/2990)) +* Update `Array` ([#2987](https://github.com/ruby/rbs/pull/2987)) +* Allow nil output buffers for reader methods ([#3002](https://github.com/ruby/rbs/pull/3002)) +* Add stdlib signature tests for #2967 ([#2989](https://github.com/ruby/rbs/pull/2989)) +* Update `Integer`, phase 1 ([#2995](https://github.com/ruby/rbs/pull/2995)) +* Add RBS::Ops ([#2934](https://github.com/ruby/rbs/pull/2934)) +* Update Module ([#2933](https://github.com/ruby/rbs/pull/2933)) +* Correct core/stdlib signatures to match Ruby 4.0 ([#2967](https://github.com/ruby/rbs/pull/2967)) +* Revert Digest::Class method changes ([#2980](https://github.com/ruby/rbs/pull/2980)) +* Add stdlib tests for updated StringScanner signatures ([#2968](https://github.com/ruby/rbs/pull/2968)) +* Update `StringScanner` signatures to match strscan 3.1.6 ([#2959](https://github.com/ruby/rbs/pull/2959)) +* Remove StringScanner methods absent from the latest Ruby release ([#2961](https://github.com/ruby/rbs/pull/2961)) +* Fix `Resolv#initialize` signature: array of resolvers + `use_ipv6:` ([#2960](https://github.com/ruby/rbs/pull/2960)) +* [Hash] Update Hash's signatures and tests to be correct ([#2694](https://github.com/ruby/rbs/pull/2694)) +* update rbs signatures to be more consistent with generics ([#2867](https://github.com/ruby/rbs/pull/2867)) +* Use top-level `path` in fileutils.rbs to suppress deprecation warning ([#2949](https://github.com/ruby/rbs/pull/2949)) +* Update File::Constants and File::Stat ([#2942](https://github.com/ruby/rbs/pull/2942)) +* Add signature for `RubyVM::InstructionSequence.of` ([#2916](https://github.com/ruby/rbs/pull/2916)) +* fix OpenSSL#session_new_cb signature ([#2924](https://github.com/ruby/rbs/pull/2924)) +* [Class] update class.rbs ([#2898](https://github.com/ruby/rbs/pull/2898)) +* Deprecate top-level float ([#2695](https://github.com/ruby/rbs/pull/2695)) +* [Kernel] Updated querying methods ([#2685](https://github.com/ruby/rbs/pull/2685)) + +### Library changes + +* Convert character offset to byte offset in parse_inline_*_annotation ([#2945](https://github.com/ruby/rbs/pull/2945)) +* Guard against underflow when sysconf(_SC_PAGESIZE) returns 0 ([#3031](https://github.com/ruby/rbs/pull/3031)) +* Report invalid UTF-8 byte in a comment as a parsing error ([#2983](https://github.com/ruby/rbs/pull/2983)) +* Fetch Chicory/ASM jars from Maven instead of bundling in gem ([#3019](https://github.com/ruby/rbs/pull/3019)) +* Add write barrier protection to RBS Location objects ([#3022](https://github.com/ruby/rbs/pull/3022)) +* Rename parameter in rbs_intern_type_name for clarity ([#3023](https://github.com/ruby/rbs/pull/3023)) +* Deprecate RDoc plugin parser ([#3016](https://github.com/ruby/rbs/pull/3016)) +* Run the test suite on JRuby (JRuby support, step 4) ([#3008](https://github.com/ruby/rbs/pull/3008)) +* Run the RBS parser on JRuby via WebAssembly (JRuby support, step 3) ([#3000](https://github.com/ruby/rbs/pull/3000)) +* Add binary serialization of the parsed AST (JRuby support, step 2) ([#2999](https://github.com/ruby/rbs/pull/2999)) +* Build the RBS parser as a WebAssembly module (JRuby support, step 1) ([#2998](https://github.com/ruby/rbs/pull/2998)) +* Reject reversed position ranges in parser/lexer entrypoints ([#2974](https://github.com/ruby/rbs/pull/2974)) +* Fix lexer infinite loop / abort on invalid UTF-8 byte ([#2973](https://github.com/ruby/rbs/pull/2973)) +* Flyweight TypeName / Namespace cached in a shared trie ([#2957](https://github.com/ruby/rbs/pull/2957)) +* Reduce Hash allocation per `Node` creation ([#2946](https://github.com/ruby/rbs/pull/2946)) +* Tidy `ast_translation.c` ([#2947](https://github.com/ruby/rbs/pull/2947)) +* Auto-generate rbs_ast_ruby_annotations_t union from config.yml ([#2939](https://github.com/ruby/rbs/pull/2939)) +* Speed up RBS::InlineParser on large sources with non-ASCII characters ([#2950](https://github.com/ruby/rbs/pull/2950)) +* Support `def self.method_name` singleton method in inline parser ([#2935](https://github.com/ruby/rbs/pull/2935)) +* This is not prism ([#2951](https://github.com/ruby/rbs/pull/2951)) +* Add RBS::Rewriter and use it in rbs annotate ([#2927](https://github.com/ruby/rbs/pull/2927)) +* Add nullability annotations to AST pointer types ([#2922](https://github.com/ruby/rbs/pull/2922)) +* Support `module-self` inline annotation ([#2921](https://github.com/ruby/rbs/pull/2921)) +* Inline instance variable annotation in module declaration ([#2919](https://github.com/ruby/rbs/pull/2919)) + +#### rbs prototype + +* Fix nested declarations in rbs prototype rbi ([#3029](https://github.com/ruby/rbs/pull/3029)) +* Deprecate prototype runtime ([#2956](https://github.com/ruby/rbs/pull/2956)) + +#### rbs collection + +* Clean up partial clone before falling back to full clone ([#2978](https://github.com/ruby/rbs/pull/2978)) +* Suppress spurious warning for non-gem stdlib libraries ([#2929](https://github.com/ruby/rbs/pull/2929)) + +### Miscellaneous + +* Fix Lint/DuplicateMethods in StringIO_test.rb ([#3010](https://github.com/ruby/rbs/pull/3010)) +* Compressed files must be opened in binary mode ([#2981](https://github.com/ruby/rbs/pull/2981)) +* Fix flaky DirSingletonTest GC failures in test_fchdir and test_for_fd ([#3004](https://github.com/ruby/rbs/pull/3004)) +* Add TruffleRuby CI job ([#2996](https://github.com/ruby/rbs/pull/2996)) +* Update the target Ruby version notes to the latest release ([#2962](https://github.com/ruby/rbs/pull/2962)) +* Update docs/inline.md to match current inline parser behavior ([#2953](https://github.com/ruby/rbs/pull/2953)) +* ci: skip Gemfile.lock BUNDLED WITH on ruby-head ([#2952](https://github.com/ruby/rbs/pull/2952)) +* Remove `logger` from sig dependencies ([#2904](https://github.com/ruby/rbs/pull/2904)) + +## 4.0.3 (2026-06-18) + +### Miscellaneous + +* Fix Ruby CI failure with compressed `Zlib::GzipReader` test fixtures. ([#3005](https://github.com/ruby/rbs/pull/3005)) +* Fix flaky `DirSingletonTest#test_fchdir` and `DirSingletonTest#test_for_fd` under aggressive GC. ([#3005](https://github.com/ruby/rbs/pull/3005)) +* Fix Ruby head CI failure caused by the lockfile-pinned Bundler version. ([#3005](https://github.com/ruby/rbs/pull/3005)) + ## 4.0.2 (2026-03-25) ### Library changes diff --git a/Dockerfile.jruby b/Dockerfile.jruby new file mode 100644 index 0000000000..106fccd5dd --- /dev/null +++ b/Dockerfile.jruby @@ -0,0 +1,53 @@ +# JRuby verification image for RBS. +# +# RBS can't load its MRI C extension on JRuby, so it parses through a +# WebAssembly build of the parser (see lib/rbs/wasm and docs/wasm_serialization.md). +# This single-stage image installs the WASI SDK, compiles rbs_parser.wasm and +# downloads the Chicory/ASM jars into ~/.m2 (via jar-dependencies), then runs the +# test suite on JRuby. It mirrors .github/workflows/jruby.yml but is self-contained. +# +# docker build -f Dockerfile.jruby -t rbs-jruby . +# docker run --rm rbs-jruby # run the test suite +# docker run --rm -e RBS_PLATFORM=java rbs-jruby \ +# gem build rbs.gemspec # build the -java gem +# +# Bundler is intentionally not used: the development Gemfile pulls in CRuby-only +# C extensions (bigdecimal, stackprof, ...) that cannot build on JRuby. The few +# gems the suite needs are installed directly, exactly as the CI does. + +FROM jruby:10.1.1.0-jdk21 + +# Keep in sync with .github/workflows/wasm.yml and .github/workflows/jruby.yml. +ARG WASI_SDK_VERSION=33 +ARG WASI_SDK_RELEASE=33.0 + +# build-essential supplies cc/make: on JRuby the prism gem builds libprism.so +# (loaded via FFI) instead of an MRI C extension, so it needs a C toolchain. +RUN apt-get update \ + && apt-get install -y --no-install-recommends git curl ca-certificates build-essential \ + && rm -rf /var/lib/apt/lists/* + +# The WASI SDK provides clang, the wasi-libc sysroot and the wasm32 compiler-rt +# builtins that `rake wasm:build` needs to compile src/**/*.c to WebAssembly. +RUN set -eux; \ + case "$(uname -m)" in \ + x86_64 | amd64) wasi_arch=x86_64 ;; \ + aarch64 | arm64) wasi_arch=arm64 ;; \ + *) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;; \ + esac; \ + mkdir -p /opt/wasi-sdk; \ + curl -fsSL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_RELEASE}-${wasi_arch}-linux.tar.gz" \ + | tar xz --strip-components=1 -C /opt/wasi-sdk +ENV WASI_SDK_PATH=/opt/wasi-sdk + +# Runtime/test gems the suite needs on JRuby (same set as the jruby.yml CI). +RUN gem install prism rake rake-compiler test-unit rdoc rspec minitest json-schema pry --no-document + +WORKDIR /rbs +COPY . . + +# Compile rbs_parser.wasm (clang is a subprocess, so the build is engine +# independent) and download the Chicory + ASM jars into ~/.m2 via jar-dependencies. +RUN rake wasm:jruby_setup wasm:install_jars + +CMD ["rake", "test"] diff --git a/Gemfile b/Gemfile index cf2982acd6..d35624b748 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,7 @@ gem "net-smtp" gem 'csv' gem 'ostruct' gem 'pstore' +gem "timeout" group :minitest do gem "minitest" diff --git a/Gemfile.lock b/Gemfile.lock index 8953a83791..de5569be0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/soutaro/steep.git - revision: 2ca2853440a667153329b87fcf27e383825dec24 + revision: 727014c361ae996a80b87948510c83ca480f5e82 specs: steep (2.1.0.dev) concurrent-ruby (>= 1.1.10) @@ -22,7 +22,7 @@ GIT PATH remote: . specs: - rbs (4.1.0.pre.1) + rbs (4.2.0.dev) logger prism (>= 1.6.0) tsort @@ -53,7 +53,7 @@ GEM ast (2.4.3) base64 (0.3.0) benchmark (0.5.0) - benchmark-ips (2.14.0) + benchmark-ips (2.15.1) bigdecimal (4.1.2) concurrent-ruby (1.3.6) connection_pool (3.0.2) @@ -62,7 +62,7 @@ GEM diff-lcs (1.6.2) digest (3.2.1) drb (2.2.3) - erb (6.0.4) + erb (6.0.6) extconf_compile_commands_json (0.0.7) ffi (1.17.4) fileutils (1.8.0) @@ -79,7 +79,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.5) + json (2.19.7) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) @@ -90,7 +90,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - marcel (1.1.0) + marcel (1.2.1) memory_profiler (1.1.0) minitest (6.0.6) drb (~> 2.0) @@ -128,9 +128,10 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (7.2.0) + rdoc (8.0.0) erb - psych (>= 4.0.0) + prism (>= 1.6.0) + rbs (>= 4.0.0) tsort regexp_parser (2.12.0) reline (0.6.3) @@ -148,7 +149,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.7) - rubocop (1.86.1) + rubocop (1.87.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -231,6 +232,7 @@ DEPENDENCIES steep! tempfile test-unit + timeout BUNDLED WITH 4.0.1 diff --git a/README.md b/README.md index 29caedb78b..85e758b4f5 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ end ## The Target Version -* The standard library signatures targets the latest release of Ruby. (`3.2` as of 2023.) -* The library code targets non-EOL versions of Ruby. (`>= 3.0` as of 2023.) +* The standard library signatures targets the [latest release of Ruby](https://www.ruby-lang.org/en/downloads/branches/). (`4.0` as of 2026.) +* The library code targets [non-EOL versions of Ruby](https://www.ruby-lang.org/en/downloads/branches/). (`>= 3.3` as of 2026.) ## Installation @@ -122,7 +122,7 @@ This is only a starting point, and you should edit the output to match your sign - `rb` generates from just the available Ruby code - `rbi` generates from Sorbet RBI -- `runtime` generates from runtime API +- `runtime` generates from runtime API (**deprecated**; see [#2841](https://github.com/ruby/rbs/issues/2841)) ## Library @@ -196,7 +196,7 @@ Here is a list of some places you can talk with active maintainers. After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +To install this gem onto your local machine, run `bundle exec rake install`. Releases are cut by the `Release gems` workflow rather than from a working copy -- see [docs/release.md](docs/release.md). ### C Code Formatting diff --git a/Rakefile b/Rakefile index 5a2e13af78..387533ed23 100644 --- a/Rakefile +++ b/Rakefile @@ -33,7 +33,14 @@ test_config = lambda do |t| end end -Rake::TestTask.new(test: :compile, &test_config) +if RUBY_ENGINE == "jruby" + # JRuby runs the parser in WebAssembly instead of the C extension, so there is + # nothing to compile. The wasm runtime must be assembled first with + # `rake wasm:jruby_setup` (which needs CRuby + the WASI SDK). + Rake::TestTask.new(:test, &test_config) +else + Rake::TestTask.new(test: :compile, &test_config) +end multitask :default => [:test, :stdlib_test, :typecheck_test, :rubocop, :validate, :test_doc] @@ -48,8 +55,15 @@ task :confirm_lexer => :lexer do end task :confirm_templates => :templates do - puts "Testing if generated code under include and src is updated with respect to templates" - sh "git diff --exit-code -- include src" + puts "Testing if generated code is updated with respect to templates" + + # Every template generates the file it is named after: `templates/.erb` generates ``. + generated = Dir.glob("templates/**/*.erb").sort.map { _1.delete_prefix("templates/").delete_suffix(".erb") } + + missing = generated.reject { File.exist?(_1) } + raise "Templates without a generated file: #{missing.join(", ")}. Is the `templates` task missing an entry?" unless missing.empty? + + sh "git diff --exit-code -- #{generated.join(" ")}" end # Task to format C code using clang-format @@ -140,6 +154,9 @@ end rule %r{^include/(.*)\.c} => 'templates/%X.c.erb' do |t| puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️" end +rule %r{^ext/(.*)\.c} => 'templates/%X.c.erb' do |t| + puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️" +end task :annotate do sh "bin/generate_docs.sh" @@ -160,6 +177,10 @@ task :templates do sh "#{ruby} templates/template.rb include/rbs/ast.h" sh "#{ruby} templates/template.rb src/ast.c" + sh "#{ruby} templates/template.rb include/rbs/serialize.h" + sh "#{ruby} templates/template.rb src/serialize.c" + sh "#{ruby} templates/template.rb lib/rbs/wasm/serialization_schema.rb" + # Format the generated files Rake::Task["format:c"].invoke end @@ -417,113 +438,431 @@ task :test_generate_stdlib do sh "ruby -c /tmp/Thread_Mutex_test.rb" end -Rake::Task[:release].enhance do - Rake::Task[:"release:note"].invoke -end +# Pull requests with one of these labels are omitted from the changelog. +CHANGELOG_SKIP_LABELS = ["skip-changelog"] -namespace :release do - desc "Explain the post-release steps automatically" - task :note do - version = Gem::Version.new(RBS::VERSION) - major, minor, patch, *_ = RBS::VERSION.split(".") - major = major.to_i - minor = minor.to_i - patch = patch.to_i +# Resolves the commit-ish the changelog starts from. +# +# `version` is a version number, a tag, or any commit-ish. When it is omitted, the latest tag +# matching `tag_glob` is used, skipping the ones matching `exclude_globs`. +# +def resolve_changelog_base(version, tag_glob:, exclude_globs: []) + require "open3" - puts "🎉🎉🎉🎉 Congratulations for **#{version}** release! 🎉🎉🎉🎉" - puts - puts "There are a few things left to complete the release. 💪" - puts + from = + if version + # `4.1.0` and `v4.1.0` both mean the tag `v4.1.0`, while `master` or a SHA is used as is. + version.match?(/\A\d/) ? "v#{version}" : version + else + command = ["git", "describe", "--tags", "--match", tag_glob, "--abbrev=0"] + exclude_globs.each { |glob| command.push("--exclude", glob) } - if patch == 0 || version.prerelease? - puts "* [ ] Update release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}" + output, status = Open3.capture2(*command) + raise "🚨 Cannot detect the latest tag matching `#{tag_glob}`. Give the previous version explicitly." unless status.success? + output.chomp end - if patch == 0 && !version.prerelease? - puts "* [ ] Delete `RBS XYZ is the latest version of...` from release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}" + _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{from}^{commit}") + raise "🚨 No such commit-ish: `#{from}`" unless status.success? + + from +end + +# Runs a GraphQL query against the repository of the working directory. +# +# `body` is the selection set inside `repository`, so a query can use the `$owner` and `$name` +# variables. Returns the contents of `data.repository`. +# +def changelog_graphql(body) + require "open3" + require "json" + + @changelog_repository ||= + begin + output, status = Open3.capture2("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner") + raise status.inspect unless status.success? + output.chomp.split("/", 2) end + owner, name = @changelog_repository - puts "* [ ] Publish a release at GitHub" - puts "* [ ] Make some announcements on Twitter/Mustdon/Slack/???" + query = <<~GRAPHQL + query($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + #{body} + } + } + GRAPHQL + + output, status = Open3.capture2( + "gh", "api", "graphql", + "-f", "query=#{query}", + "-f", "owner=#{owner}", + "-f", "name=#{name}", + binmode: true + ) + raise status.inspect unless status.success? - puts - puts + # GitHub always answers in UTF-8, while the default external encoding follows the locale. Without + # this, a pull request body with an emoji fails to parse under `LANG=C`, as in GitHub Actions. + JSON.parse(output.force_encoding(Encoding::UTF_8), symbolize_names: true).dig(:data, :repository) +end - puts "✏️ Making a draft release on GitHub..." +# Lists the commits between `from` and `HEAD`, newest first. +# +# Giving `paths` limits the commits to the ones touching the paths. +# +def changelog_commits(from, paths: []) + require "open3" - content = File.read(File.join(__dir__, "CHANGELOG.md")) - changelog = content.scan(/^## \d.*?(?=^## \d)/m)[0] - changelog = changelog.sub(/^.*\n^.*\n/, "").rstrip + command = ["git", "log", "--format=%H", "#{from}..HEAD"] + # `--simplify-merges` keeps the default history simplification from following only one parent of + # a merge commit, which can drop the other side. Note that `--full-history` alone is wrong here: + # it also lists merge commits that do not touch the paths, bringing back the excluded pull + # requests. The two flags produce the same commits as the default mode for this repository today. + command.push("--full-history", "--simplify-merges", "--", *paths) unless paths.empty? - notes = < [pull request, ...] }`, with an empty array for the commits GitHub has no +# merged pull request for, including the ones it does not know at all. +# +def changelog_associated_pull_requests(oids) + oids.uniq.each_slice(50).each_with_object({}) do |slice, found| + aliases = slice.map.with_index do |oid, index| + <<~GRAPHQL + c#{index}: object(oid: "#{oid}") { + ... on Commit { + associatedPullRequests(first: 10) { + nodes { + number title url merged + labels(first: 100) { nodes { name } } + } + } + } + } + GRAPHQL end - require "open3" - output, status = Open3.capture2(*command) - if status.success? - puts " >> Done! Open #{output.chomp} and publish the release!" + response = changelog_graphql(aliases.join("\n")) + + slice.each_with_index do |oid, index| + nodes = response.dig(:"c#{index}", :associatedPullRequests, :nodes) || [] + + found[oid] = nodes.select { |pr| pr[:merged] }.map do |pr| + { number: pr[:number], title: pr[:title], url: pr[:url], labels: pr.dig(:labels, :nodes).map { |label| label[:name] } } + end end end end +# Finds the pull requests the commits came from, keeping the order of `commits`. +# +# Returns the pull requests for the changelog and the ones omitted by `skip_labels`. +# +def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS) + origins = changelog_origins(commits) + found = changelog_associated_pull_requests(commits.map { |commit| origins[commit] || commit }) + + # An origin that leads nowhere -- a commit cherry-picked from a fork, or one that went to the + # default branch without a pull request -- falls back to the commit in this history, which is + # at least the backport that brought it here. + fallbacks = commits.select { |commit| origins[commit] && found.fetch(origins[commit], []).empty? } + found.update(changelog_associated_pull_requests(fallbacks)) unless fallbacks.empty? + + pull_requests = {} + skipped = {} + + commits.each do |commit| + prs = found.fetch(origins[commit] || commit, []) + prs = found.fetch(commit, []) if prs.empty? + + prs.each do |pr| + if (pr[:labels] & skip_labels).empty? + pull_requests[pr[:number]] ||= pr + else + skipped[pr[:number]] ||= pr + end + end + end -desc "Generate changelog template from GH pull requests" -task :changelog do - major, minor, patch, _pre = RBS::VERSION.split(".", 4) - major = major.to_i - minor = minor.to_i - patch = patch.to_i + [pull_requests.values, skipped.values] +end - if patch == 0 - milestone = "RBS #{major}.#{minor}" - else - milestone = "RBS #{major}.#{minor}.x" +# Fetches the details that help classifying the pull requests: the changed files and the body. +# +def changelog_pull_request_details(pull_requests) + pull_requests.each_slice(50).flat_map do |slice| + aliases = slice.map do |pr| + <<~GRAPHQL + p#{pr[:number]}: pullRequest(number: #{pr[:number]}) { + body + author { login } + files(first: 100) { + nodes { path } + pageInfo { hasNextPage } + } + } + GRAPHQL + end + + details = changelog_graphql(aliases.join("\n")) + + slice.map do |pr| + detail = details[:"p#{pr[:number]}"] or next pr + + pr.merge( + author: detail.dig(:author, :login), + # The body is a hint for writing the changelog, not a copy source. Keep it short. + body: detail[:body].to_s.strip.slice(0, 1000), + files: detail.dig(:files, :nodes).map { |file| file[:path] }, + files_truncated: detail.dig(:files, :pageInfo, :hasNextPage) + ) + end end +end - puts "🔍 Finding pull requests that is associated to milestone `#{milestone}`..." +# Reports the pull requests omitted by their label, so that they do not disappear silently. +# +def warn_skipped_pull_requests(skipped, skip_labels) + return if skipped.empty? - command = [ - "gh", - "pr", - "list", - "--limit=10000", - "--json", - "url,title,number", - "--search" , - "milestone:\"#{milestone}\" is:merged sort:updated-desc -label:Released" - ] + numbers = skipped.map { |pr| "##{pr[:number]}" } + numbers = numbers.take(20).push("and #{numbers.size - 20} more") if numbers.size > 20 - require "open3" - output, status = Open3.capture2(*command) - raise status.inspect unless status.success? + $stderr.puts + $stderr.puts " (⏭️ Skipped #{skipped.size} pull request(s) labeled #{skip_labels.map { |label| "`#{label}`" }.join(" or ")}: #{numbers.join(", ")})" +end + +# Prints the changelog template listing the pull requests merged between `from` and `HEAD`. +# +# The changelog goes to STDOUT and everything else goes to STDERR, so that the output can be +# piped to another command: `rake gem:changelog | pbcopy` +# +def print_changelog(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS) + $stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..." + + commits = changelog_commits(from, paths: paths) + if commits.empty? + $stderr.puts " (🤔 There is no commit after `#{from}`.)" + return + end + + pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels) + + if pull_requests.empty? + $stderr.puts " (🤔 No pull request is associated to the commits after `#{from}`.)" + else + $stderr.puts + pull_requests.each do |pr| + puts "* #{pr[:title]} ([##{pr[:number]}](#{pr[:url]}))" + end + $stdout.flush + end + warn_skipped_pull_requests(skipped, skip_labels) +end + +# Prints the same pull requests as `print_changelog` as JSON, with the details that help +# classifying them into the sections of CHANGELOG.md. +# +# This is the input for the release automation, so it always prints a valid JSON document. +# +def print_changelog_json(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS) require "json" - json = JSON.parse(output, symbolize_names: true) - unless json.empty? - puts - json.each do |line| - puts "* #{line[:title]} ([##{line[:number]}](#{line[:url]}))" + $stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..." + + commits = changelog_commits(from, paths: paths) + pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels) + pull_requests = changelog_pull_request_details(pull_requests) + + $stderr.puts " (📋 #{pull_requests.size} pull request(s))" + + puts JSON.pretty_generate( + { + from: from, + to: "HEAD", + pull_requests: pull_requests, + skipped: skipped + } + ) + $stdout.flush + + warn_skipped_pull_requests(skipped, skip_labels) +end + +namespace :gem do + # The gem is developed in the whole repository except the Rust crate, which has its own release + # cycle. Note that this is an *exclusion*, not a list of the directories shipped in the gem: + # changes in `test/` or `.github/` are part of the gem's changelog too. + # A constant defined in a `namespace` block is a top-level constant, so it needs the prefix. + GEM_CHANGELOG_PATHS = [".", ":(exclude)rust"] + + # The tags a release proper starts *after*, rather than at. + GEM_PRERELEASE_TAGS = ["v*.pre*", "v*.dev*"] + + # Where the changelog of the release being prepared starts, derived from `RBS::VERSION`: + # + # * `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1`, so it starts from the latest tag. + # * `X.Y.Z` documents the whole cycle, the prereleases included, so it skips the prerelease tags + # in between and starts from the previous release proper. + # + # This is the step that is easy to get wrong by hand: on a release proper the latest tag is a + # prerelease, so the obvious default would produce only the tail of the cycle. Passing a version + # explicitly overrides all of it. + # + def changelog_base(version) + excluded = Gem::Version.new(RBS::VERSION).prerelease? ? [] : GEM_PRERELEASE_TAGS + resolve_changelog_base(version, tag_glob: "v*", exclude_globs: excluded) + end + + desc "Generate changelog template from GH pull requests merged since the previous release" + task :changelog, [:version] do |_task, args| + print_changelog(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS) + end + + namespace :changelog do + desc "Print the pull requests of `gem:changelog` as JSON, with the changed files and body of each" + task :json, [:version] do |_task, args| + print_changelog_json(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS) end - else - puts " (🤑 There is no *unreleased* pull request associated to the milestone.)" + end + + # There are three kinds of release: `X.Y.Z`, `X.Y.Z.pre.N`, and `X.Y.Z.dev.N`. The + # `.dev.N` ones are cut from the development line for people who need a specific + # change early; they are not written up in the changelog, so there are no notes to + # publish and nothing worth announcing. + def dev_release?(version) + Gem::Version.new(version).segments.include?("dev") + end + + # The body of the topmost section of CHANGELOG.md, which is the release being + # prepared, minus its own heading. + # + # The encoding is explicit because the default external encoding follows the + # locale, and the changelog is not ASCII. + # + def changelog_section(version) + content = File.read(File.join(__dir__, "CHANGELOG.md"), encoding: Encoding::UTF_8) + section = content.scan(/^## \d.*?(?=^## \d)/m)[0] or raise "🚨 Cannot find a release section in CHANGELOG.md" + heading, _, body = section.partition("\n") + heading.include?(version) or raise "🚨 CHANGELOG.md starts with `#{heading.strip}`, which is not #{version}" + body.strip + end + + desc "Check that the working tree is ready to be released as the given version" + task :check_release, [:version] do |_task, args| + version = args[:version] or raise "🚨 Pass the version being released: `rake 'gem:check_release[4.1.2]'`" + Gem::Version.correct?(version) or raise "🚨 `#{version}` is not a version number." + + # The version being released and the version the commit declares are stated + # separately -- one by whoever starts the release, one by the commit itself -- + # so that releasing the wrong commit, or releasing the right one under the wrong + # name, fails here rather than on RubyGems. + version == RBS::VERSION or + raise "🚨 Releasing #{version}, but this commit declares `RBS::VERSION = #{RBS::VERSION.inspect}`." + + if dev_release?(version) + puts "✅ #{version} is the version of this commit. It is a dev release, so CHANGELOG.md is not checked." + else + changelog_section(version) + puts "✅ #{version} is the version of this commit, and CHANGELOG.md documents it." + end + end + + desc "Create and push the `vX.Y.Z` tag for RBS::VERSION" + task :tag do + tag = "v#{RBS::VERSION}" + + # Annotated, so that the tag carries its own author and date rather than + # borrowing the tagged commit's. + sh "git", "tag", "--annotate", "--message", "RBS #{RBS::VERSION}", tag + sh "git", "push", "origin", tag + + puts "🏷️ Pushed #{tag}." + end + + desc "Publish the GitHub release for RBS::VERSION, unless it is a `.dev.` version" + task :gh_release do + require "open3" + + version = Gem::Version.new(RBS::VERSION) + major, minor, *_ = RBS::VERSION.split(".") + tag = "v#{RBS::VERSION}" + + if dev_release?(RBS::VERSION) + puts "⏭️ #{RBS::VERSION} is a dev release, so there is no GitHub release to publish." + next + end + + # The release is created against an existing tag, so that the artifacts and the + # notes describe a commit that is already immutable. + _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{tag}^{commit}") + raise "🚨 No such tag: `#{tag}`. Tag the release before creating the GitHub release." unless status.success? + + notes = <<~NOTES + [Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}) + + #{changelog_section(RBS::VERSION)} + NOTES + + # Published rather than drafted: the notes are the changelog section that was + # already reviewed in the release pull request, so there is nothing left to edit. + command = [ + "gh", "release", "create", tag, + "--title=#{RBS::VERSION}", + "--notes=#{notes}" + ] + command << "--prerelease" if version.prerelease? + + output, status = Open3.capture2(*command) + raise "🚨 `gh release create` failed: #{status.inspect}" unless status.success? + + puts "📝 Released #{tag}: #{output.chomp}" end end @@ -549,6 +888,104 @@ task :prepare_profiling do Rake::Task[:"compile"].invoke end +namespace :wasm do + WASM_DIR = File.expand_path("wasm", __dir__) + WASM_OUTPUT = File.join(WASM_DIR, "rbs_parser.wasm") + + # The parser under src/ is plain, self-contained C with no dependency on the + # Ruby C API, so it can be compiled to WebAssembly as-is. The only extra + # translation unit is the entry-point shim under wasm/. + def wasm_source_files + Dir.glob(File.join(__dir__, "src/**/*.c")).sort + [File.join(WASM_DIR, "rbs_wasm.c")] + end + + # Locate the clang shipped with the WASI SDK. + # + # The system clang can target wasm32, but the WASI SDK additionally provides + # the wasi-libc sysroot and the wasm32 compiler-rt builtins that the link + # step needs, so we require it explicitly. + def wasi_clang + sdk = ENV["WASI_SDK_PATH"] + if sdk.nil? || sdk.empty? + raise <<~MSG + WASI_SDK_PATH is not set. + + Install the WASI SDK from https://github.com/WebAssembly/wasi-sdk/releases + and point WASI_SDK_PATH at the extracted directory, for example: + + export WASI_SDK_PATH=/opt/wasi-sdk + rake wasm:build + MSG + end + + clang = File.join(sdk, "bin", "clang") + raise "clang not found at #{clang} (is WASI_SDK_PATH correct?)" unless File.executable?(clang) + + clang + end + + desc "Build the RBS parser as a WebAssembly module (requires WASI_SDK_PATH)" + task :build do + # `-DNDEBUG` compiles out `RBS_ASSERT`, the same way ext/rbs_extension does + # for the MRI extension. The assertions sit in the lexer and the constant + # pool, so keeping them costs about 20% of parse time; set `DEBUG=1` to keep + # them when debugging the module itself. + debug_flags = ENV["DEBUG"] ? [] : ["-DNDEBUG"] + + mkdir_p WASM_DIR + sh wasi_clang, + "--target=wasm32-wasip1", + # No `main`; the host calls `_initialize` and then the exported functions. + "-mexec-model=reactor", + "-std=gnu11", + "-O2", + *debug_flags, + "-Wno-unused-parameter", + "-I#{File.join(__dir__, "include")}", + "-o", WASM_OUTPUT, + *wasm_source_files + puts "Built #{WASM_OUTPUT}" + end + + desc "Build and smoke-test the WebAssembly module (requires wasmtime)" + task :check => :build do + wasmtime = ENV["WASMTIME"] || "wasmtime" + + # `rbs_wasm_selftest` parses a small fixed signature and returns 1 on + # success. `--invoke` prints the return value to stdout. + output = IO.popen([wasmtime, "run", "--invoke", "rbs_wasm_selftest", WASM_OUTPUT], err: File::NULL, &:read).to_s.strip + + if output == "1" + puts "WebAssembly selftest passed." + else + raise "WebAssembly selftest failed: rbs_wasm_selftest returned #{output.inspect} (expected \"1\")" + end + end + + # Where the runtime looks for the module by default (see RBS::WASM::Runtime). + JRUBY_WASM_DIR = File.expand_path("lib/rbs/wasm", __dir__) + + desc "Download the Chicory/ASM jars into the local Maven repository (~/.m2). Run on JRuby." + task :install_jars do + # Resolves the `jar` requirements from rbs.gemspec via Maven and downloads + # them (and their transitive deps) into ~/.m2, the same way `gem install` + # does; the jars are not copied into the gem. The platform is forced to java + # because Jars::Installer skips non-java gems, and write_require_file is false + # because lib/rbs_jars.rb is hand-maintained (the generator mangles the + # `com.dylibso.chicory:runtime` artifact id). + require "jars/installer" + spec = Gem::Specification.load("rbs.gemspec") + spec.platform = "java" + Jars::Installer.new(spec).install_jars(write_require_file: false) + end + + desc "Build rbs_parser.wasm and copy it next to RBS::WASM::Runtime" + task :jruby_setup => [:build] do + cp WASM_OUTPUT, File.join(JRUBY_WASM_DIR, "rbs_parser.wasm") + puts "rbs_parser.wasm is ready under #{JRUBY_WASM_DIR}" + end +end + namespace :rust do namespace :rbs do RUST_DIR = File.expand_path("rust", __dir__) diff --git a/Steepfile b/Steepfile index b00e4542b0..35285dc956 100644 --- a/Steepfile +++ b/Steepfile @@ -6,6 +6,15 @@ target :lib do ignore( "lib/rbs/test", # "lib/rbs/test.rb" + + # JRuby-only implementations of RBS::Location and RBS::Parser. Like the C + # extension, these implement interfaces already described in sig/, and + # runtime.rb is Java interop, so they are not type-checked here. + "lib/rbs/wasm/location.rb", + "lib/rbs/wasm/runtime.rb", + "lib/rbs/wasm/parser.rb", + # jar-dependencies require_jar calls for the JRuby runtime; not type-checked. + "lib/rbs_jars.rb", ) library "pathname", "json", "logger", "monitor", "tsort", "uri", 'dbm', 'pstore', 'singleton', 'shellwords', 'fileutils', 'find', 'digest', 'prettyprint', 'yaml', "psych", "securerandom" diff --git a/config.yml b/config.yml index 8551779f3f..7eac7f2195 100644 --- a/config.yml +++ b/config.yml @@ -769,6 +769,7 @@ nodes: c_type: rbs_location_range - name: type_name c_type: rbs_type_name + optional: true # NULL when the name is left out, to be inferred from the Ruby code - name: type_name_location c_type: rbs_location_range optional: true @@ -781,6 +782,7 @@ nodes: c_type: rbs_location_range - name: type_name c_type: rbs_type_name + optional: true # NULL when the name is left out, to be inferred from the Ruby code - name: type_name_location c_type: rbs_location_range optional: true diff --git a/core/array.rbs b/core/array.rbs index 03d3f13a63..000dba738a 100644 --- a/core/array.rbs +++ b/core/array.rbs @@ -546,8 +546,14 @@ # given block. # %a{annotate:rdoc:source:from=array.c} -class Array[unchecked out Elem] < Object - include Enumerable[Elem] +class Array[unchecked out E] + include Enumerable[E] + + # Interface for random number generations; used with `Array#shuffle`, `Array#shuffle!`, and `Array#sample`. + interface _Rand + # Generate a random number up to, but excluding, `max` + def rand: (Integer max) -> int + end # # Appends each argument in `objects` to `self`; returns `self`: @@ -1187,7 +1195,7 @@ class Array[unchecked out Elem] < Object # Related: Array#rassoc; see also [Methods for # Fetching](rdoc-ref:Array@Methods+for+Fetching). # - def assoc: (untyped) -> ::Array[untyped]? + def assoc: (untyped object) -> Array[untyped]? # where E: _ToAry[X], object: _Equal # + # Returns the first element for which the block returns a truthy value. + # + # With a block given, calls the block with successive elements of the array; + # returns the first element for which the block returns a truthy value: + # + # [1, 3, 5].find {|element| element > 2} # => 3 + # + # If no such element is found, calls `if_none_proc` and returns its return + # value. + # + # [1, 3, 5].find(proc {-1}) {|element| element > 12} # => -1 + # + # With no block given, returns an Enumerator. + # + alias detect find # # With a block given, calls the block with each element of `self`; returns a new @@ -2007,8 +2032,7 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Fetching](rdoc-ref:Array@Methods+for+Fetching). # - def filter: () { (Elem item) -> boolish } -> ::Array[Elem] - | () -> ::Enumerator[Elem, ::Array[Elem]] + alias filter select # # With a block given, calls the block with each element of `self`; removes from @@ -2025,8 +2049,7 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Deleting](rdoc-ref:Array@Methods+for+Deleting). # - def filter!: () { (Elem item) -> boolish } -> self? - | () -> ::Enumerator[Elem, self?] + alias filter! select! # + # Freezes `self` (if not already frozen); returns `self`: + # + # a = [] + # a.frozen? # => false + # a.freeze + # a.frozen? # => true + # + # No further changes may be made to `self`; raises FrozenError if a change is + # attempted. + # + # Related: Kernel#frozen?. + # + def freeze: () -> self # # Returns the zero-based integer index of a specified element, or `nil`. @@ -2278,7 +2319,7 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Assigning](rdoc-ref:Array@Methods+for+Assigning). # - def insert: (int index, *Elem obj) -> self + def insert: (int index, *E objects) -> self # # With a block given, calls the block with each element of `self`; returns a new @@ -2502,10 +2543,12 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Fetching](rdoc-ref:Array@Methods+for+Fetching). # - def max: %a{implicitly-returns-nil} () -> Elem - | %a{implicitly-returns-nil} () { (Elem a, Elem b) -> ::Integer? } -> Elem - | (int n) -> ::Array[Elem] - | (int n) { (Elem a, Elem b) -> ::Integer? } -> ::Array[Elem] + def max: %a{implicitly-returns-nil} () -> E # where E: Comparable::_WithSpaceshipOperator + | %a{implicitly-returns-nil} () { (E a, E b) -> Comparable::_CompareToZero } -> E + | %a{implicitly-returns-nil} %a{warning: returning `nil` will always raise at runtime} () { (E a, E b) -> Comparable::_CompareToZero? } -> E + | (int count) -> Array[E] # where E: Comparable::_WithSpaceshipOperator + | (int count) { (E a, E b) -> Comparable::_CompareToZero } -> Array[E] + | %a{warning: returning `nil` will always raise at runtime} (int count) { (E a, E b) -> Comparable::_CompareToZero? } -> Array[E] # # Prepends the given `objects` to `self`: @@ -2803,10 +2856,14 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Combining](rdoc-ref:Array@Methods+for+Combining). # - def product: () -> ::Array[[ Elem ]] - | [X] (::Array[X] other_ary) -> ::Array[[ Elem, X ]] - | [X, Y] (::Array[X] other_ary1, ::Array[Y] other_ary2) -> ::Array[[ Elem, X, Y ]] - | [U] (*::Array[U] other_arys) -> ::Array[::Array[Elem | U]] + def product: () -> Array[[ E ]] + | () { ([E] combination) -> void } -> self + | [X] (array[X] other_ary) -> Array[[ E, X ]] + | [X] (array[X] other_ary) { ([E, X] combination) -> void } -> self + | [X, Y] (array[X] other_ary1, array[Y] other_ary2) -> Array[[ E, X, Y ]] + | [X, Y] (array[X] other_ary1, array[Y] other_ary2) { ([E, X, Y] combination) -> void } -> self + | [U] (*array[U] other_arys) -> Array[Array[E | U]] + | [U] (*array[U] other_arys) { (Array[E | U] combination) -> void } -> self # # Replaces the elements of `self` with the elements of `other_array`, which must @@ -2976,7 +3033,7 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Assigning](rdoc-ref:Array@Methods+for+Assigning). # - def replace: (::Array[Elem]) -> self + def replace: (array[E] other_array) -> self # # Returns the count of elements in `self`: @@ -3464,9 +3521,7 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Fetching](rdoc-ref:Array@Methods+for+Fetching). # - def slice: %a{implicitly-returns-nil} (int index) -> Elem - | (int start, int length) -> ::Array[Elem]? - | (::Range[::Integer] range) -> ::Array[Elem]? + alias slice [] # # Returns the new string formed by calling method #inspect on each @@ -3797,7 +3854,7 @@ class Array[unchecked out Elem] < Object # # Related: see [Methods for Converting](rdoc-ref:Array@Methods+for+Converting). # - def transpose: () -> ::Array[::Array[untyped]] + def transpose: () -> Array[Array[untyped]] # # Returns an array of flattened objects returned by the block. @@ -399,8 +399,8 @@ module Enumerable[unchecked out Elem] : _Each[Elem] # # Alias: #collect_concat. # - def collect_concat: [U] () { (Elem) -> (::Array[U] | U) } -> ::Array[U] - | () -> ::Enumerator[Elem, ::Array[untyped]] + def collect_concat: [U] () { (E) -> (::Array[U] | U) } -> ::Array[U] + | () -> ::Enumerator[E, ::Array[untyped]] # # Returns the first element for which the block returns a truthy value. @@ -488,8 +488,8 @@ module Enumerable[unchecked out Elem] : _Each[Elem] # # With no block given, returns an Enumerator. # - def detect: (?Proc ifnone) { (Elem) -> boolish } -> Elem? - | (?Proc ifnone) -> ::Enumerator[Elem, Elem?] + def detect: (?Proc ifnone) { (E) -> boolish } -> E? + | (?Proc ifnone) -> ::Enumerator[E, E?] # # Returns an array containing the items in `self`: # # (0..4).to_a # => [0, 1, 2, 3, 4] # - def entries: () -> ::Array[Elem] + def entries: () -> ::Array[E] def enum_for: (Symbol method, *untyped, **untyped) ?{ (?) -> Integer } -> Enumerator[untyped, untyped] - | () ?{ () -> Integer } -> Enumerator[Elem, self] + | () ?{ () -> Integer } -> Enumerator[E, self] %a{annotate:rdoc:skip} alias to_enum enum_for @@ -639,8 +639,8 @@ module Enumerable[unchecked out Elem] : _Each[Elem] # # Related: #reject. # - def find_all: () { (Elem) -> boolish } -> ::Array[Elem] - | () -> ::Enumerator[Elem, ::Array[Elem]] + def find_all: () { (E) -> boolish } -> ::Array[E] + | () -> ::Enumerator[E, ::Array[E]] # # Returns an array containing elements selected by the block. @@ -697,8 +697,8 @@ module Enumerable[unchecked out Elem] : _Each[Elem] # With no argument and no block given, returns an Enumerator. # def find_index: (untyped value) -> Integer? - | () { (Elem) -> boolish } -> Integer? - | () -> ::Enumerator[Elem, Integer?] + | () { (E) -> boolish } -> Integer? + | () -> ::Enumerator[E, Integer?] # # Returns whether for any element object == element: @@ -972,8 +972,8 @@ module Enumerable[unchecked out Elem] : _Each[Elem] # def inject: (untyped init, Symbol method) -> untyped | (Symbol method) -> untyped - | [A] (A initial) { (A, Elem) -> A } -> A - | () { (Elem, Elem) -> Elem } -> Elem + | [A] (A initial) { (A, E) -> A } -> A + | () { (E, E) -> E } -> E # # Returns an array of objects returned by the block. @@ -1645,8 +1645,8 @@ module Enumerable[unchecked out Elem] : _Each[Elem] # # With no block given, returns an Enumerator. # - def map: [U] () { (Elem arg0) -> U } -> ::Array[U] - | () -> ::Enumerator[Elem, ::Array[untyped]] + def map: [U] () { (E arg0) -> U } -> ::Array[U] + | () -> ::Enumerator[E, ::Array[untyped]] # # Iterates the given block for each element with an arbitrary object, `obj`, and @@ -541,17 +541,17 @@ class Enumerator[unchecked out Elem, out Return = void] < Object # # => foo: 1 # # => foo: 2 # - def with_object: [U] (U obj) { (Elem, U obj) -> untyped } -> U - | [U] (U obj) -> ::Enumerator[[ Elem, U ], U] + def with_object: [U] (U obj) { (E, U obj) -> untyped } -> U + | [U] (U obj) -> ::Enumerator[[ E, U ], U] end # # Generator # -class Enumerator::Generator[out Elem] < Object - include Enumerable[Elem] +class Enumerator::Generator[out E] < Object + include Enumerable[E] - def each: () { (Elem) -> void } -> void + def each: () { (E) -> void } -> void end # @@ -620,7 +620,7 @@ end # # This returns an array of items like a normal enumerator does. # all_checked = active_items.select(&:checked) # -class Enumerator::Lazy[out Elem, out Return = void] < Enumerator[Elem, Return] +class Enumerator::Lazy[out E, out R = void] < Enumerator[E, R] # # Expands `lazy` enumerator to an array. See Enumerable#to_a. # @@ -632,7 +632,7 @@ class Enumerator::Lazy[out Elem, out Return = void] < Enumerator[Elem, Return] # --> # Like Enumerable#compact, but chains operation to be lazy-evaluated. # - def compact: () -> Enumerator::Lazy[Elem, Return] + def compact: () -> Enumerator::Lazy[E, R] # # Returns a non-lazy Enumerator converted from the lazy enumerator. # - def eager: () -> ::Enumerator[Elem, Return] + def eager: () -> ::Enumerator[E, R] end # @@ -675,7 +675,7 @@ end # # This type of objects can be created by Enumerable#chain and Enumerator#+. # -class Enumerator::Chain[out Elem] < Enumerator[Elem, void] +class Enumerator::Chain[out E] < Enumerator[E, void] # # Enumerator::Product generates a Cartesian product of any number of enumerable # objects. Iterating over the product of enumerable objects is roughly @@ -30,7 +30,7 @@ class Enumerator[unchecked out Elem, out Return = void] # # This type of objects can be created by Enumerator.product. # - class Product[unchecked out Elem] < Enumerator[Array[Elem], Product[Elem]] + class Product[unchecked out E] < Enumerator[Array[E], Product[E]] # # The number of base digits for the `double` data type. # @@ -1292,25 +1290,3 @@ Float::NAN: Float # decimal. # Float::RADIX: Integer - -# Deprecated, do not use. -# -# Represents the rounding mode for floating point addition at the start time. -# -# Usually defaults to 1, rounding to the nearest number. -# -# Other modes include: -# -# -1 -# : Indeterminable -# 0 -# : Rounding towards zero -# 1 -# : Rounding to the nearest number -# 2 -# : Rounding towards positive infinity -# 3 -# : Rounding towards negative infinity -# -# -Float::ROUNDS: Integer diff --git a/core/hash.rbs b/core/hash.rbs index 0a56bb66f4..871c688895 100644 --- a/core/hash.rbs +++ b/core/hash.rbs @@ -469,15 +469,29 @@ # * #transform_values: Returns a copy of `self` with modified values. # * #transform_values!: Modifies values in `self`. # -class Hash[unchecked out K, unchecked out V] < Object +class Hash[unchecked out K, unchecked out V] include Enumerable[[ K, V ]] + # Interface that indicates a type can be used as a key to `Hash` lookup methods. + # interface _Key def hash: () -> Integer def eql?: (untyped rhs) -> boolish end + # Interface that indicates a type convertible to `[ K, V ]` via its `#to_ary` method. + # + interface _Pair[K, V] + def to_ary: () -> [ K, V ] + end + + # Interface for comparing equality of types. + # + interface _Equals + def ==: (untyped other) -> boolish + end + # # - def deconstruct_keys: (Array[K] | nil) -> self + def deconstruct_keys: (untyped) -> self # # With a block given, calls the block with each key-value pair; returns `self`: @@ -1015,8 +1031,8 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Iterating](rdoc-ref:Hash@Methods+for+Iterating). # - def each: () { ([ K, V ] arg0) -> untyped } -> self - | () -> ::Enumerator[[ K, V ], self] + def each: () -> Enumerator[[ K, V ], self] + | () { ([ K, V ] pair) -> void } -> self # + # # With a block given, calls the block with each entry's key and value; returns a # new hash whose entries are those for which the block returns a truthy value: # @@ -1200,10 +1220,14 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Deleting](rdoc-ref:Hash@Methods+for+Deleting). # - def filter: () { (K, V) -> boolish } -> ::Hash[K, V] - | () -> ::Enumerator[[ K, V ], ::Hash[K, V]] + def select: () -> Enumerator[[ K, V ], Hash[K, V]] + | () { (K key, V value) -> boolish } -> Hash[K, V] - # + # # With a block given, calls the block with each entry's key and value; removes # from `self` each entry for which the block returns `false` or `nil`. # @@ -1217,8 +1241,8 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Deleting](rdoc-ref:Hash@Methods+for+Deleting). # - def filter!: () { (K, V) -> boolish } -> self? - | () -> ::Enumerator[[ K, V ], self?] + def select!: () -> Enumerator[[ K, V ], self?] + | () { (K key, V value) -> boolish } -> self? # # Returns whether `key` is a key in `self`: @@ -1267,7 +1291,7 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: [Methods for Querying](rdoc-ref:Hash@Methods+for+Querying). # - def has_key?: (_Key) -> bool + def has_key?: (_Key key) -> bool # # Returns whether `key` is a key in `self`: @@ -1403,16 +1427,19 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Fetching](rdoc-ref:Hash@Methods+for+Fetching). # - def keys: () -> ::Array[K] + def keys: () -> Array[K] - # + # # Returns the count of entries in `self`: # # {foo: 0, bar: 1, baz: 2}.size # => 3 # # Related: see [Methods for Querying](rdoc-ref:Hash@Methods+for+Querying). # - def length: () -> Integer + def size: () -> Integer # # Returns whether `key` is a key in `self`: @@ -1467,8 +1494,8 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Assigning](rdoc-ref:Hash@Methods+for+Assigning). # - def merge: [A, B] (*::Hash[A, B] other_hashes) -> ::Hash[A | K, B | V] - | [A, B, C] (*::Hash[A, B] other_hashes) { (K key, V oldval, B newval) -> C } -> ::Hash[A | K, B | V | C] + def merge: [A, B] (*hash[A, B] other_hashes) -> Hash[A | K, B | V] + | [A, B, C] (*hash[A, B] other_hashes) { (K key, V oldval, B newval) -> C } -> Hash[A | K, B | V | C] # # Updates values and/or adds entries to `self`; returns `self`. @@ -1511,8 +1538,8 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Assigning](rdoc-ref:Hash@Methods+for+Assigning). # - def merge!: (*::Hash[K, V] other_hashes) -> self - | (*::Hash[K, V] other_hashes) { (K key, V oldval, V newval) -> V } -> self + def merge!: (*hash[K, V] other_hashes) -> self + | (*hash[K, V] other_hashes) { (K key, V oldval, V newval) -> V } -> self # # Replaces the entire contents of `self` with the contents of `other_hash`; @@ -1602,13 +1631,9 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Assigning](rdoc-ref:Hash@Methods+for+Assigning). # - def replace: (Hash[K, V]) -> self + def replace: (hash[K, V] other) -> self - # + # # With a block given, calls the block with each entry's key and value; returns a # new hash whose entries are those for which the block returns a truthy value: # @@ -1619,13 +1644,9 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Deleting](rdoc-ref:Hash@Methods+for+Deleting). # - alias select filter + alias filter select - # + # # With a block given, calls the block with each entry's key and value; removes # from `self` each entry for which the block returns `false` or `nil`. # @@ -1639,7 +1660,7 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Deleting](rdoc-ref:Hash@Methods+for+Deleting). # - alias select! filter! + alias filter! select! # + # # Returns the count of entries in `self`: # # {foo: 0, bar: 1, baz: 2}.size # => 3 # # Related: see [Methods for Querying](rdoc-ref:Hash@Methods+for+Querying). # - alias size length + alias length size # # Associates the given `object` with the given `key`; returns `object`. @@ -1723,7 +1741,7 @@ class Hash[unchecked out K, unchecked out V] < Object # # Related: see [Methods for Converting](rdoc-ref:Hash@Methods+for+Converting). # - def to_a: () -> ::Array[[ K, V ]] + def to_a: () -> Array[[ K, V ]] # # Returns `1`. # - def denominator: () -> Integer + def denominator: () -> 1 # # Returns a string containing the place-value representation of `self` in radix @@ -964,14 +965,14 @@ class Integer < Numeric # 3.lcm(-7) #=> 21 # ((1<<31)-1).lcm((1<<61)-1) #=> 4951760154835678088235319297 # - def lcm: (Integer) -> Integer + def lcm: (Integer other_int) -> Integer # # - def magnitude: () -> Integer + alias magnitude abs # # Returns `self` modulo `other` as a real numeric (Integer, Float, or Rational). @@ -1009,7 +1010,7 @@ class Integer < Numeric # # Related: Integer#pred (predecessor value). # - def next: () -> Integer + alias next succ # # Returns `self`. # - def numerator: () -> Integer + def numerator: () -> self # # Returns `self`; intended for compatibility to character literals in Ruby 1.9. # - def ord: () -> Integer + def ord: () -> self def polar: () -> [ Integer, Integer | Float ] @@ -1180,8 +1181,7 @@ class Integer < Numeric # # Related: Integer#truncate. # - def round: (?half: :up | :down | :even) -> Integer - | (int digits, ?half: :up | :down | :even) -> (Integer | Float) + def round: (?int digits, ?half: Numeric::round_mode) -> Integer # # Returns `self` (which is already an Integer). # - def to_i: () -> Integer + def to_i: () -> self # + # Returns pathname configuration variable using fpathconf(). + # + # *name* should be a constant under `Etc` which begins with `PC_`. + # + # The return value is an integer or nil. nil means indefinite limit. + # (fpathconf() returns -1 but errno is not set.) + # + # require 'etc' + # IO.pipe {|r, w| + # p w.pathconf(Etc::PC_PIPE_BUF) #=> 4096 + # } + # + def pathconf: (Integer name) -> Integer? + # # Evaluates the given block in the context of the class/module. The method @@ -417,7 +416,7 @@ class Module < Object # # Hello there! # - def class_exec: [U] (*untyped, **untyped) { (?) [self: self] -> U } -> U + alias class_exec module_exec # # Invokes Module.append_features on each parameter in reverse order. # - def include: (Module, *Module arg0) -> self + def include: (Module module, *Module additional_modules) -> self # # Invokes Module.prepend_features on each parameter in reverse order. # - def prepend: (Module, *Module arg0) -> self + def prepend: (Module module, *Module additional_modules) -> self # # Makes a list of existing constants private. # - def private_constant: (*interned arg0) -> self + def private_constant: (*interned names) -> self # # Makes a list of existing constants public. # - def public_constant: (*interned arg0) -> self + def public_constant: (*interned names) -> self # # Returns a string representing this module or class. For basic classes and # modules, this is the name. For singletons, we show information on the thing # we're attached to as well. # - def inspect: () -> String + alias inspect to_s # # Returns `true` if `key` is a key in `self`, otherwise `false`. # - def key?: (Key) -> bool + def key?: (K) -> bool end end diff --git a/core/pathname.rbs b/core/pathname.rbs index cdcfc82bab..6ab6614c08 100644 --- a/core/pathname.rbs +++ b/core/pathname.rbs @@ -234,11 +234,6 @@ class Pathname # def +: (Pathname | String | _ToStr other) -> Pathname - # - # alias / + # - # def ===: (untyped) -> bool # - # alias << send # - # alias << send # +# Workaround for directly loading Gem::Version in some cases +# module Gem # # Raised when RubyGems is unable to load or activate a gem. Contains the name diff --git a/core/rubygems/requirement.rbs b/core/rubygems/requirement.rbs index 8b20e12996..ecea717da2 100644 --- a/core/rubygems/requirement.rbs +++ b/core/rubygems/requirement.rbs @@ -123,18 +123,8 @@ module Gem # def satisfied_by?: (Gem::Version version) -> bool - # - # alias === satisfied_by? - # - # alias =~ satisfied_by? # +# Workaround for directly loading Gem::Version in some cases +# module Gem interface _HashLike[K, V] def each_pair: () { ([ K, V ]) -> untyped } -> self diff --git a/core/rubygems/specification.rbs b/core/rubygems/specification.rbs index a6d74630e9..1162e0d450 100644 --- a/core/rubygems/specification.rbs +++ b/core/rubygems/specification.rbs @@ -19,5 +19,13 @@ # #metadata for restrictions on the format and size of metadata items you may # add to a specification. # +# Specifications must be deterministic, as in the example above. For instance, +# you cannot define attributes conditionally: +# +# # INVALID: do not do this. +# unless RUBY_ENGINE == "jruby" +# s.extensions << "ext/example/extconf.rb" +# end +# class Gem::Specification < Gem::BasicSpecification end diff --git a/core/rubygems/version.rbs b/core/rubygems/version.rbs index 7aec81635a..e13073926d 100644 --- a/core/rubygems/version.rbs +++ b/core/rubygems/version.rbs @@ -1,165 +1,5 @@ %a{annotate:rdoc:skip} module Gem - # - # The Version class processes string versions into comparable values. A version - # string should normally be a series of numbers separated by periods. Each part - # (digits separated by periods) is considered its own number, and these are used - # for sorting. So for instance, 3.10 sorts higher than 3.2 because ten is - # greater than two. - # - # If any part contains letters (currently only a-z are supported) then that - # version is considered prerelease. Versions with a prerelease part in the Nth - # part sort less than versions with N-1 parts. Prerelease parts are sorted - # alphabetically using the normal Ruby string sorting rules. If a prerelease - # part contains both letters and numbers, it will be broken into multiple parts - # to provide expected sort behavior (1.0.a10 becomes 1.0.a.10, and is greater - # than 1.0.a9). - # - # Prereleases sort between real releases (newest to oldest): - # - # 1. 1.0 - # 2. 1.0.b1 - # 3. 1.0.a.2 - # 4. 0.9 - # - # If you want to specify a version restriction that includes both prereleases - # and regular releases of the 1.x series this is the best way: - # - # s.add_dependency 'example', '>= 1.0.0.a', '< 2.0.0' - # - # ## How Software Changes - # - # Users expect to be able to specify a version constraint that gives them some - # reasonable expectation that new versions of a library will work with their - # software if the version constraint is true, and not work with their software - # if the version constraint is false. In other words, the perfect system will - # accept all compatible versions of the library and reject all incompatible - # versions. - # - # Libraries change in 3 ways (well, more than 3, but stay focused here!). - # - # 1. The change may be an implementation detail only and have no effect on the - # client software. - # 2. The change may add new features, but do so in a way that client software - # written to an earlier version is still compatible. - # 3. The change may change the public interface of the library in such a way - # that old software is no longer compatible. - # - # Some examples are appropriate at this point. Suppose I have a Stack class - # that supports a `push` and a `pop` method. - # - # ### Examples of Category 1 changes: - # - # * Switch from an array based implementation to a linked-list based - # implementation. - # * Provide an automatic (and transparent) backing store for large stacks. - # - # ### Examples of Category 2 changes might be: - # - # * Add a `depth` method to return the current depth of the stack. - # * Add a `top` method that returns the current top of stack (without changing - # the stack). - # * Change `push` so that it returns the item pushed (previously it had no - # usable return value). - # - # ### Examples of Category 3 changes might be: - # - # * Changes `pop` so that it no longer returns a value (you must use `top` to - # get the top of the stack). - # * Rename the methods to `push_item` and `pop_item`. - # - # ## RubyGems Rational Versioning - # - # * Versions shall be represented by three non-negative integers, separated by - # periods (e.g. 3.1.4). The first integers is the "major" version number, - # the second integer is the "minor" version number, and the third integer is - # the "build" number. - # - # * A category 1 change (implementation detail) will increment the build - # number. - # - # * A category 2 change (backwards compatible) will increment the minor - # version number and reset the build number. - # - # * A category 3 change (incompatible) will increment the major build number - # and reset the minor and build numbers. - # - # * Any "public" release of a gem should have a different version. Normally - # that means incrementing the build number. This means a developer can - # generate builds all day long, but as soon as they make a public release, - # the version must be updated. - # - # ### Examples - # - # Let's work through a project lifecycle using our Stack example from above. - # - # Version 0.0.1 - # : The initial Stack class is release. - # - # Version 0.0.2 - # : Switched to a linked=list implementation because it is cooler. - # - # Version 0.1.0 - # : Added a `depth` method. - # - # Version 1.0.0 - # : Added `top` and made `pop` return nil (`pop` used to return the old top - # item). - # - # Version 1.1.0 - # : `push` now returns the value pushed (it used it return nil). - # - # Version 1.1.1 - # : Fixed a bug in the linked list implementation. - # - # Version 1.1.2 - # : Fixed a bug introduced in the last fix. - # - # - # Client A needs a stack with basic push/pop capability. They write to the - # original interface (no `top`), so their version constraint looks like: - # - # gem 'stack', '>= 0.0' - # - # Essentially, any version is OK with Client A. An incompatible change to the - # library will cause them grief, but they are willing to take the chance (we - # call Client A optimistic). - # - # Client B is just like Client A except for two things: (1) They use the `depth` - # method and (2) they are worried about future incompatibilities, so they write - # their version constraint like this: - # - # gem 'stack', '~> 0.1' - # - # The `depth` method was introduced in version 0.1.0, so that version or - # anything later is fine, as long as the version stays below version 1.0 where - # incompatibilities are introduced. We call Client B pessimistic because they - # are worried about incompatible future changes (it is OK to be pessimistic!). - # - # ## Preventing Version Catastrophe: - # - # From: - # https://www.zenspider.com/ruby/2008/10/rubygems-how-to-preventing-catastrophe. - # html - # - # Let's say you're depending on the fnord gem version 2.y.z. If you specify your - # dependency as ">= 2.0.0" then, you're good, right? What happens if fnord 3.0 - # comes out and it isn't backwards compatible with 2.y.z? Your stuff will break - # as a result of using ">=". The better route is to specify your dependency with - # an "approximate" version specifier ("~>"). They're a tad confusing, so here is - # how the dependency specifiers work: - # - # Specification From ... To (exclusive) - # ">= 3.0" 3.0 ... ∞ - # "~> 3.0" 3.0 ... 4.0 - # "~> 3.0.0" 3.0.0 ... 3.1 - # "~> 3.5" 3.5 ... 4.0 - # "~> 3.5.0" 3.5.0 ... 3.6 - # "~> 3" 3.0 ... 4.0 - # - # For the last example, single-digit versions are automatically extended with a - # zero to give a sensible result. - # class Version include Comparable diff --git a/core/set.rbs b/core/set.rbs index cecad36d80..6c17d91930 100644 --- a/core/set.rbs +++ b/core/set.rbs @@ -738,7 +738,7 @@ class Set[unchecked out A] end %a{annotate:rdoc:skip} -module Enumerable[unchecked out Elem] +module Enumerable[unchecked out E] # # Returns the values in `self` as an array: @@ -441,8 +441,8 @@ class Struct[Elem] # # Related: #each_pair. # - def each: () -> Enumerator[Elem, self] - | () { (Elem value) -> void } -> self + def each: () -> Enumerator[E, self] + | () { (E value) -> void } -> self # # With a block given, returns an array of values from `self` for which the block @@ -591,7 +591,7 @@ class Struct[Elem] # Raises RangeError if any element of the range is negative and out of range; # see Array@Array+Indexes. # - def values_at: (*int | range[int?] positions) -> Array[Elem] + def values_at: (*int | range[int?] positions) -> Array[E] # @@ -664,5 +664,5 @@ class Struct[Elem] # h = joe.deconstruct_keys(nil) # h # => {:name=>"Joseph Smith, Jr.", :address=>"123 Maple, Anytown NC", :zip=>12345} # - def deconstruct_keys: (Array[index & Hash::_Key]? indices) -> Hash[index & Hash::_Key, Elem] + def deconstruct_keys: (Array[index & Hash::_Key]? indices) -> Hash[index & Hash::_Key, E] end diff --git a/core/thread.rbs b/core/thread.rbs index d47803fc6c..c5da4e6ffd 100644 --- a/core/thread.rbs +++ b/core/thread.rbs @@ -1565,10 +1565,10 @@ class Thread::Mutex < Object # - # Attempts to grab the lock and waits if it isn't available. Raises - # `ThreadError` if `mutex` was locked by the current thread. + # Releases the lock. Raises `ThreadError` if `mutex` wasn't locked by the + # current thread. # def unlock: () -> self end @@ -1604,7 +1604,7 @@ end # # consumer.join # -class Thread::Queue[Elem = untyped] < Object +class Thread::Queue[E = untyped] < Object # # Pushes the given `object` to the queue. # @@ -1719,7 +1719,7 @@ class Thread::Queue[Elem = untyped] < Object # If `timeout` seconds have passed and no data is available `nil` is returned. # If `timeout` is `0` it returns immediately. # - def pop: (?boolish non_block, ?timeout: _ToF?) -> Elem? + def pop: (?boolish non_block, ?timeout: _ToF?) -> E? # # Pushes the given `object` to the queue. # - def push: (Elem obj) -> void + def push: (E obj) -> void # - # +class Thread::SizedQueue[E = untyped] < Thread::Queue[E] alias << push # - # :method: freeze Freeze both the object returned by `__getobj__` and self. + # Freeze both the object returned by `__getobj__` and self. # def freeze: () -> self @@ -118,6 +118,7 @@ class Delegator < BasicObject # rdoc-file=lib/delegate.rb # - method_missing(m, *args, &block) # --> + # Handles the magic of delegation through `__getobj__`. # def method_missing: (Symbol m, *untyped args, **untyped) { (*untyped, **untyped) -> untyped } -> untyped diff --git a/stdlib/digest/0/digest.rbs b/stdlib/digest/0/digest.rbs index b0d7a350a0..cd72b513e1 100644 --- a/stdlib/digest/0/digest.rbs +++ b/stdlib/digest/0/digest.rbs @@ -303,7 +303,7 @@ module Digest::Instance # uninitialized. Do not call this method from outside. Use #digest!() instead, # which ensures that internal data be reset for security reasons. # - def finish: () -> self + def finish: () -> String end # @@ -536,13 +536,19 @@ class Digest::SHA2 < Digest::Class # def update: (String) -> self - private def finish: () -> String - # + # Finishes the digest and returns the resulting hash value. # + # This method is overridden by each implementation subclass and often made + # private, because some of those subclasses may leave internal data + # uninitialized. Do not call this method from outside. Use #digest!() instead, + # which ensures that internal data be reset for security reasons. + # + private def finish: () -> String + alias << update # # Returns system temporary directory; typically "/tmp". # - def self?.systmpdir: () -> ::String + def self?.systmpdir: () -> ::String? # - # alias === include? # # See #as_json. # - def self.json_create: [Elem] (Hash[String, String | Array[Elem]] object) -> Struct[Elem] + def self.json_create: [E] (Hash[String, String | Array[E]] object) -> Struct[E] # # Calls wait repeatedly until the given block yields a truthy value. # @@ -346,7 +346,7 @@ class MonitorMixin::ConditionVariable # # Calls wait repeatedly while the given block yields a truthy value. # diff --git a/stdlib/openssl/0/openssl.rbs b/stdlib/openssl/0/openssl.rbs index ebfde30722..83046e972f 100644 --- a/stdlib/openssl/0/openssl.rbs +++ b/stdlib/openssl/0/openssl.rbs @@ -2100,7 +2100,7 @@ module OpenSSL # # See IO#read for full details. # - def read: (?Integer? size, ?String buf) -> String? + def read: (?Integer? size, ?String? buf) -> String? # # The "sync mode" of the SSLSocket. @@ -5913,10 +5913,17 @@ module OpenSSL def initialize_copy: (self) -> void end - # - # Generic exception that is raised if an operation on a DH PKey fails - # unexpectedly or in case an instantiation of an instance of DH fails due to - # non-conformant input data. + # + # Raised when errors occur during PKey#sign or PKey#verify. + # + # Before version 4.0.0, OpenSSL::PKey::PKeyError had the following subclasses. + # These subclasses have been removed and the constants are now defined as + # aliases of OpenSSL::PKey::PKeyError. + # + # * OpenSSL::PKey::DHError + # * OpenSSL::PKey::DSAError + # * OpenSSL::PKey::ECError + # * OpenSSL::PKey::RSAError # class DHError < OpenSSL::PKey::PKeyError end @@ -6304,10 +6311,17 @@ module OpenSSL def initialize_copy: (self) -> void end - # - # Generic exception that is raised if an operation on a DSA PKey fails - # unexpectedly or in case an instantiation of an instance of DSA fails due to - # non-conformant input data. + # + # Raised when errors occur during PKey#sign or PKey#verify. + # + # Before version 4.0.0, OpenSSL::PKey::PKeyError had the following subclasses. + # These subclasses have been removed and the constants are now defined as + # aliases of OpenSSL::PKey::PKeyError. + # + # * OpenSSL::PKey::DHError + # * OpenSSL::PKey::DSAError + # * OpenSSL::PKey::ECError + # * OpenSSL::PKey::RSAError # class DSAError < OpenSSL::PKey::PKeyError end @@ -7011,6 +7025,18 @@ module OpenSSL end end + # + # Raised when errors occur during PKey#sign or PKey#verify. + # + # Before version 4.0.0, OpenSSL::PKey::PKeyError had the following subclasses. + # These subclasses have been removed and the constants are now defined as + # aliases of OpenSSL::PKey::PKeyError. + # + # * OpenSSL::PKey::DHError + # * OpenSSL::PKey::DSAError + # * OpenSSL::PKey::ECError + # * OpenSSL::PKey::RSAError + # class ECError < OpenSSL::PKey::PKeyError end @@ -9235,7 +9261,7 @@ module OpenSSL # Reads *length* bytes from the SSL connection. If a pre-allocated *buffer* is # provided the data will be written into it. # - def sysread: (Integer length, ?String buffer) -> String + def sysread: (Integer length, ?String? buffer) -> String # # Creates a new SSL socket from *io* which must be a real IO object (not an # IO-like object that responds to read/write). @@ -9289,6 +9316,10 @@ module OpenSSL # If *ctx* is provided the SSL Sockets initial params will be taken from the # context. # + # The optional *sync_close* keyword parameter sets the *sync_close* instance + # variable. Setting this to `true` will cause the underlying socket to be closed + # when the SSL/TLS connection is shut down. + # # The OpenSSL::Buffering module provides additional IO methods. # # This method will freeze the SSLContext if one is provided; however, session @@ -10390,11 +10421,6 @@ module OpenSSL extend OpenSSL::Marshal::ClassMethods - # - # def ==: (self other) -> bool # - # def ==: (self other) -> bool # - # def ==: (self other) -> bool # - # def ==: (untyped other) -> bool # - # def ==: (untyped other) -> bool # diff --git a/stdlib/shellwords/0/shellwords.rbs b/stdlib/shellwords/0/shellwords.rbs index 7412f7a3a7..e2d206240e 100644 --- a/stdlib/shellwords/0/shellwords.rbs +++ b/stdlib/shellwords/0/shellwords.rbs @@ -191,7 +191,7 @@ module Shellwords end %a{annotate:rdoc:skip} -class Array[unchecked out Elem] +class Array[unchecked out E] # # Raises NotImplementedError. # - def fcntl: (Integer integer_cmd, String | Integer arg) -> Integer + def fcntl: (*untyped) -> bot # # Returns 0; for compatibility with IO. # - def fsync: () -> Integer? + def fsync: () -> Integer # # Returns `nil`; for compatibility with IO. # - def internal_encoding: () -> Encoding + def internal_encoding: () -> nil # # See IO#read. # - def read: (?int? length, ?string outbuf) -> String? + def read: (?int? length, ?string? outbuf) -> String? - def read_nonblock: (int len, ?string buf) -> String + # + # See IO#pread. + # + def pread: (Integer maxlen, Integer offset, ?String? outbuf) -> String + + def read_nonblock: (int len, ?string? buf, ?exception: bool) -> String? def readbyte: () -> Integer @@ -1301,7 +1311,7 @@ class StringIO # def readlines: (?String sep, ?Integer limit, ?chomp: boolish) -> ::Array[String] - def readpartial: (int maxlen, ?string outbuf) -> String + def readpartial: (int maxlen, ?string? outbuf) -> String # + # Sets the encoding according to the BOM (Byte Order Mark) in the string. + # + # Returns `self` if the BOM is found, otherwise +nil. + # + def set_encoding_by_bom: () -> Encoding? + # - # Equivalent to #terminate. This method is obsolete; use #terminate instead. - # - def clear: () -> void + def check_until: (Regexp | String) -> String? # - # Equivalent to #eos?. This method is obsolete, use #eos? instead. - # - def empty?: () -> bool - # - # Equivalent to #get_byte. This method is obsolete; use #get_byte instead. - # - def getbyte: () -> String? - # + # Returns the array of captured match values at indexes (1..) + # if the most recent match attempt succeeded, or nil otherwise; + # see [Captured Match Values](rdoc-ref:StringScanner@Captured+Match+Values): + # scanner = StringScanner.new('Fri Dec 12 1975 14:39') + # scanner.named_captures # => {} + # + # pattern = /(?\w+) (?\w+) (?\d+) / + # scanner.match?(pattern) + # scanner.named_captures # => {"wday"=>"Fri", "month"=>"Dec", "day"=>"12"} + # + # scanner.string = 'nope' + # scanner.match?(pattern) + # scanner.named_captures # => {"wday"=>nil, "month"=>nil, "day"=>nil} + # + # scanner.match?(/nosuch/) + # scanner.named_captures # => {} + # + def named_captures: () -> Hash[String, String?] + # - # Equivalent to #peek. This method is obsolete; use #peek instead. + # Peeks at the current byte and returns it as an integer. + # + # s = StringScanner.new('ab') + # s.peek_byte # => 97 # - def peep: (Integer) -> String + def peek_byte: () -> Integer? # # call-seq: @@ -1124,7 +1126,7 @@ class StringScanner # scanner.match?(/nope/) # => nil # scanner.post_match # => nil # - def post_match: () -> String + def post_match: () -> String? # - # `s.restsize` is equivalent to `s.rest_size`. This method is obsolete; use - # #rest_size instead. - # - def restsize: () -> Integer - # + # Scans one byte and returns it as an integer. This method is not multibyte + # character sensitive. See also: #getch. + # + def scan_byte: () -> Integer? # + # If `base` isn't provided or is `10`, then it is equivalent to calling + # #scan with a `[+-]?d+` pattern, and returns an Integer or nil. + # + # If `base` is `16`, then it is equivalent to calling #scan with a + # `[+-]?(0x)?[0-9a-fA-F]+` pattern, and returns an Integer or nil. + # + # The scanned string must be encoded with an ASCII compatible encoding, + # otherwise Encoding::CompatibilityError will be raised. + # + def scan_integer: (?base: Integer) -> Integer? # # def self?.timeout: [T] (Numeric? sec, ?singleton(Exception) klass, ?String message) { (Numeric sec) -> T } -> T end diff --git a/stdlib/tsort/0/cyclic.rbs b/stdlib/tsort/0/cyclic.rbs index 8fa44fd7ff..11162b36e3 100644 --- a/stdlib/tsort/0/cyclic.rbs +++ b/stdlib/tsort/0/cyclic.rbs @@ -1,5 +1,5 @@ %a{annotate:rdoc:skip} -module TSort[Node] +module TSort[N] # # Exception class to be raised when a cycle is found. # diff --git a/stdlib/tsort/0/interfaces.rbs b/stdlib/tsort/0/interfaces.rbs index d560681a4f..8d410d9397 100644 --- a/stdlib/tsort/0/interfaces.rbs +++ b/stdlib/tsort/0/interfaces.rbs @@ -1,20 +1,20 @@ %a{annotate:rdoc:skip} -module TSort[Node] - interface _Sortable[Node] +module TSort[N] + interface _Sortable[N] # #tsort_each_node is used to iterate for all nodes over a graph. # - def tsort_each_node: () { (Node) -> void } -> void + def tsort_each_node: () { (N) -> void } -> void # #tsort_each_child is used to iterate for child nodes of node. # - def tsort_each_child: (Node) { (Node) -> void } -> void + def tsort_each_child: (N) { (N) -> void } -> void end - interface _EachNode[Node] - def call: () { (Node) -> void } -> void + interface _EachNode[N] + def call: () { (N) -> void } -> void end - interface _EachChild[Node] - def call: (Node) { (Node) -> void } -> void + interface _EachChild[N] + def call: (N) { (N) -> void } -> void end end diff --git a/stdlib/tsort/0/tsort.rbs b/stdlib/tsort/0/tsort.rbs index ec5b55cd2c..521f284a9b 100644 --- a/stdlib/tsort/0/tsort.rbs +++ b/stdlib/tsort/0/tsort.rbs @@ -113,7 +113,7 @@ # 1. Tarjan, "Depth First Search and Linear Graph Algorithms", # *SIAM Journal on Computing*, Vol. 1, No. 2, pp. 146-160, June 1972. # -module TSort[Node] : TSort::_Sortable[Node] +module TSort[N] : TSort::_Sortable[N] # - # alias + merge # :stopdoc: diff --git a/stdlib/zlib/0/gzip_reader.rbs b/stdlib/zlib/0/gzip_reader.rbs index 1e94cd4b74..68631933d0 100644 --- a/stdlib/zlib/0/gzip_reader.rbs +++ b/stdlib/zlib/0/gzip_reader.rbs @@ -187,7 +187,7 @@ module Zlib # --> # See Zlib::GzipReader documentation for a description. # - def read: (?int? length, ?string outbuf) -> String? + def read: (?int? length, ?string? outbuf) -> String? # - # Flushes input buffer and returns all data in that buffer. # def flush_next_in: () -> String diff --git a/steep/Gemfile.lock b/steep/Gemfile.lock index ef9ad39469..702ce9138c 100644 --- a/steep/Gemfile.lock +++ b/steep/Gemfile.lock @@ -25,7 +25,7 @@ GEM fileutils (1.8.0) i18n (1.14.8) concurrent-ruby (~> 1.0) - json (2.19.5) + json (2.19.9) language_server-protocol (3.17.0.5) listen (3.10.0) logger diff --git a/templates/ext/rbs_extension/ast_translation.c.erb b/templates/ext/rbs_extension/ast_translation.c.erb index b933153293..d9480bd30a 100644 --- a/templates/ext/rbs_extension/ast_translation.c.erb +++ b/templates/ext/rbs_extension/ast_translation.c.erb @@ -114,6 +114,30 @@ VALUE <%= enum.translator_name %>(<%= enum.c_type_name %> value) { rb_class_new_instance(argc, argv, receiver) #endif +// Route Namespace / TypeName construction through the Ruby-side +// flyweight cache (`RBS::Namespace.[]` / `RBS::TypeName.[]`) so that +// structurally equal values produced by the parser share canonical +// instances. An in-C trie walk was tried but did not beat +// `rb_funcallv` on Ruby 4.0+, where method dispatch is well optimized. +static ID id_intern_brackets; + +static inline ID intern_brackets(void) { + if (!id_intern_brackets) id_intern_brackets = rb_intern("[]"); + return id_intern_brackets; +} + +static VALUE rbs_intern_namespace(rbs_translation_context_t ctx, rbs_namespace_t *node) { + VALUE args[2]; + args[0] = rbs_node_list_to_ruby_array(ctx, node->path); + args[1] = node->absolute ? Qtrue : Qfalse; + return rb_funcallv(RBS_Namespace, intern_brackets(), 2, args); +} + +static VALUE rbs_intern_type_name(VALUE type_namespace, VALUE name) { + VALUE args[2] = { type_namespace, name }; + return rb_funcallv(RBS_TypeName, intern_brackets(), 2, args); +} + VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instance) { if (instance == NULL) return Qnil; @@ -152,6 +176,15 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan rb_ary_push(array, rbs_node_list_to_ruby_array(ctx, signature->directives)); rb_ary_push(array, rbs_node_list_to_ruby_array(ctx, signature->declarations)); return array; + + <%- when "RBS::Namespace" -%> + return rbs_intern_namespace(ctx, (rbs_namespace_t *) instance); + + <%- when "RBS::TypeName" -%> + rbs_type_name_t *node = (rbs_type_name_t *) instance; + VALUE ns = rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->rbs_namespace); + VALUE name = rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->name); + return rbs_intern_type_name(ns, name); <%- else -%> <%= node.c_type_name %> *node = (<%= node.c_type_name %> *) instance; @@ -160,14 +193,16 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan <%- if node.locations -%> VALUE arg_location = rbs_location_range_to_ruby_location(ctx, node->base.location); rbs_loc *loc = rbs_check_location(arg_location); - rbs_loc_legacy_alloc_children(loc, <%= node.locations.size %>); + { + rbs_loc_legacy_alloc_children(loc, <%= node.locations.size %>); <%- node.locations.each do |location_field| -%> <%- if location_field.required? -%> - rbs_loc_legacy_add_required_child(loc, rb_intern("<%= location_field.name %>"), (rbs_loc_range) { .start = node-><%= location_field.attribute_name %>.start_char, .end = node-><%= location_field.attribute_name %>.end_char }); + rbs_loc_legacy_add_required_child(loc, rb_intern("<%= location_field.name %>"), (rbs_loc_range) { .start = node-><%= location_field.attribute_name %>.start_char, .end = node-><%= location_field.attribute_name %>.end_char }); <%- else -%> - rbs_loc_legacy_add_optional_child(loc, rb_intern("<%= location_field.name %>"), (rbs_loc_range) { .start = node-><%= location_field.attribute_name %>.start_char, .end = node-><%= location_field.attribute_name %>.end_char }); + rbs_loc_legacy_add_optional_child(loc, rb_intern("<%= location_field.name %>"), (rbs_loc_range) { .start = node-><%= location_field.attribute_name %>.start_char, .end = node-><%= location_field.attribute_name %>.end_char }); <%- end -%> <%- end -%> + } <%- else -%> VALUE arg_location = rbs_location_range_to_ruby_location(ctx, node->base.location); <%- end -%> @@ -225,11 +260,7 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan <%- node.fields.each do |field| -%> rb_hash_aset(h, ID2SYM(rb_intern("<%= field.name %>")), arg_<%= field.name %>); <%- end -%> - return CLASS_NEW_INSTANCE( - <%= node.c_constant_name %>, - 1, - &h - ); + return CLASS_NEW_INSTANCE(<%= node.c_constant_name %>, 1, &h); <%- end -%> } <%- end -%> diff --git a/templates/include/rbs/serialize.h.erb b/templates/include/rbs/serialize.h.erb new file mode 100644 index 0000000000..572a5f616b --- /dev/null +++ b/templates/include/rbs/serialize.h.erb @@ -0,0 +1,32 @@ +#ifndef RBS__SERIALIZE_H +#define RBS__SERIALIZE_H + +#include "rbs/ast.h" +#include "rbs/string.h" +#include "rbs/util/rbs_allocator.h" +#include "rbs/util/rbs_constant_pool.h" + +/** + * Serialize a parsed AST node into a compact, portable binary buffer. + * + * The format is consumed by RBS::WASM::Deserializer on the Ruby side, which + * rebuilds the same `RBS::AST` objects that the C extension would have built + * directly. This is what lets RBS run on Ruby implementations that cannot load + * the C extension (notably JRuby): the parser runs inside WebAssembly, produces + * this buffer, and the host reconstructs the tree in pure Ruby. + * + * The buffer is allocated from `allocator`, so its lifetime is tied to that + * allocator. `constant_pool` must be the pool the node was parsed with; it is + * used to resolve interned symbol/identifier ids back into their bytes. + * + * See `docs/wasm_serialization.md` for the wire format. + */ +rbs_string_t rbs_serialize_node(rbs_allocator_t *allocator, rbs_constant_pool_t *constant_pool, rbs_node_t *node); + +/** + * Like rbs_serialize_node, but for a bare node list (e.g. the result of + * rbs_parse_type_params). Decoded by RBS::WASM::Deserializer.deserialize_node_list. + */ +rbs_string_t rbs_serialize_node_list(rbs_allocator_t *allocator, rbs_constant_pool_t *constant_pool, rbs_node_list_t *list); + +#endif diff --git a/templates/lib/rbs/wasm/serialization_schema.rb.erb b/templates/lib/rbs/wasm/serialization_schema.rb.erb new file mode 100644 index 0000000000..df96adcd7f --- /dev/null +++ b/templates/lib/rbs/wasm/serialization_schema.rb.erb @@ -0,0 +1,82 @@ +<%- + # Node types that need `RBS::AST::TypeParam.resolve_variables` applied to + # their `type_params` before construction (matches ast_translation.c.erb). + resolve_type_params = %w[ + RBS::AST::Declarations::Class + RBS::AST::Declarations::Module + RBS::AST::Declarations::Interface + RBS::AST::Declarations::TypeAlias + RBS::MethodType + ] + + # How the deserializer should read a single field, derived from its c_type. + reader_for = ->(field) { + case field.type.name + when "rbs_node_list" then ":node_list" + when "rbs_hash" then ":hash" + when "rbs_string" then ":string" + when "bool" then ":bool" + when "rbs_location_range" then ":location_range" + when "rbs_location_range_list" then ":location_range_list" + when "rbs_attr_ivar_name" then ":attr_ivar_name" + else + if field.type.is_a?(RBS::Template::EnumType) + symbols = [] + field.type.descr.each_symbol { |_sym, _const, ruby_value| symbols << (ruby_value.nil? ? "nil" : ":#{ruby_value}") } + "[:enum, [#{symbols.join(", ")}]]" + else + ":node" + end + end + } + + entry_for = ->(node) { + case node.ruby_full_name + when "RBS::AST::Bool" then "[:bool]" + when "RBS::AST::Integer" then "[:integer]" + when "RBS::AST::String" then "[:string_value]" + when "RBS::Types::Record::FieldType" then "[:record_field]" + when "RBS::Signature" then "[:signature]" + when "RBS::Namespace" then "[:namespace]" + when "RBS::TypeName" then "[:type_name]" + else + children = (node.locations || []).map { |loc| "[:#{loc.name}, #{loc.required?}]" } + fields = node.fields.map { |field| "[:#{field.name}, #{reader_for.call(field)}]" } + # Emit `nil` rather than `[]` for empty lists, so the generated file has no + # unannotated empty collections for Steep to flag. + children_literal = children.empty? ? "nil" : "[#{children.join(", ")}]" + fields_literal = fields.empty? ? "nil" : "[#{fields.join(", ")}]" + "[:node, #{node.ruby_full_name.inspect}, #{node.expose_location?}, #{children_literal}, #{fields_literal}, #{resolve_type_params.include?(node.ruby_full_name)}]" + end + } +-%> +module RBS + module WASM + # Describes how to decode the binary buffer produced by `rbs_serialize_node` + # (src/serialize.c) back into RBS::AST objects. RBS::WASM::Deserializer walks + # this table; the matching encoder is generated from the same config.yml, so + # the two stay in sync. + # + # SCHEMA is indexed by node tag: tag 0 is NULL and SYMBOL_TAG is the + # interned-symbol tag. Each remaining entry is one of: + # + # [:node, class_name, expose_location, loc_children, fields, resolve_type_params] + # [:bool] / [:integer] / [:string_value] / [:record_field] + # [:signature] / [:namespace] / [:type_name] + # + # where loc_children is [[name, required?], ...] and fields is + # [[name, reader], ...] with reader one of :node, :node_list, :hash, :string, + # :bool, :location_range, :location_range_list, :attr_ivar_name, or + # [:enum, [value_or_nil, ...]]. + module SerializationSchema + SYMBOL_TAG = <%= nodes.size + 1 %> + + SCHEMA = [ + nil, # tag 0 is reserved for NULL + <%- nodes.each do |node| -%> + <%= entry_for.call(node) %>, + <%- end -%> + ].freeze + end + end +end diff --git a/templates/src/serialize.c.erb b/templates/src/serialize.c.erb new file mode 100644 index 0000000000..c72d2feccc --- /dev/null +++ b/templates/src/serialize.c.erb @@ -0,0 +1,233 @@ +#include "rbs/serialize.h" + +#include "rbs/location.h" +#include "rbs/util/rbs_assert.h" +#include "rbs/util/rbs_buffer.h" + +#include + +/** + * State threaded through the recursive serializer: the arena the output buffer + * grows in, the constant pool used to resolve interned ids, and the buffer + * itself. + */ +typedef struct { + rbs_allocator_t *allocator; + rbs_constant_pool_t *constant_pool; + rbs_buffer_t buffer; +} rbs_serialize_state; + +/* All multi-byte integers are written little-endian. */ + +static void w_bytes(rbs_serialize_state *state, const char *value, size_t length) { + if (length > 0) { + rbs_buffer_append_string(state->allocator, &state->buffer, value, length); + } +} + +static void w_u8(rbs_serialize_state *state, uint8_t value) { + w_bytes(state, (const char *) &value, 1); +} + +static void w_u32(rbs_serialize_state *state, uint32_t value) { + unsigned char bytes[4] = { + (unsigned char) (value & 0xff), + (unsigned char) ((value >> 8) & 0xff), + (unsigned char) ((value >> 16) & 0xff), + (unsigned char) ((value >> 24) & 0xff), + }; + w_bytes(state, (const char *) bytes, 4); +} + +static void w_i32(rbs_serialize_state *state, int32_t value) { + w_u32(state, (uint32_t) value); +} + +static void w_string(rbs_serialize_state *state, rbs_string_t string) { + size_t length = rbs_string_len(string); + w_u32(state, (uint32_t) length); + w_bytes(state, string.start, length); +} + +static void w_constant(rbs_serialize_state *state, rbs_constant_id_t id) { + rbs_constant_t *constant = rbs_constant_pool_id_to_constant(state->constant_pool, id); + RBS_ASSERT(constant != NULL, "constant is NULL"); + w_u32(state, (uint32_t) constant->length); + w_bytes(state, (const char *) constant->start, constant->length); +} + +// A location range is encoded as a presence byte, followed by the start/end +// character positions when present. A null range encodes as a single 0 byte +// (it becomes `nil` on the Ruby side). +static void w_loc_range(rbs_serialize_state *state, rbs_location_range range) { + if (RBS_LOCATION_NULL_RANGE_P(range)) { + w_u8(state, 0); + } else { + w_u8(state, 1); + w_i32(state, range.start_char); + w_i32(state, range.end_char); + } +} + +static void w_loc_range_list(rbs_serialize_state *state, rbs_location_range_list_t *list) { + if (list == NULL) { + w_u32(state, 0); + return; + } + + w_u32(state, (uint32_t) list->length); + for (rbs_location_range_list_node_t *n = list->head; n != NULL; n = n->next) { + w_loc_range(state, n->range); + } +} + +static void w_attr_ivar_name(rbs_serialize_state *state, rbs_attr_ivar_name_t ivar_name) { + w_u8(state, (uint8_t) ivar_name.tag); + if (ivar_name.tag == RBS_ATTR_IVAR_NAME_TAG_NAME) { + w_constant(state, ivar_name.name); + } +} + +static void serialize_node(rbs_serialize_state *state, rbs_node_t *instance); + +static void w_node_list(rbs_serialize_state *state, rbs_node_list_t *list) { + if (list == NULL) { + w_u32(state, 0); + return; + } + + w_u32(state, (uint32_t) list->length); + for (rbs_node_list_node_t *n = list->head; n != NULL; n = n->next) { + serialize_node(state, n->node); + } +} + +static void w_hash(rbs_serialize_state *state, rbs_hash_t *hash) { + if (hash == NULL) { + w_u32(state, 0); + return; + } + + // rbs_hash_t does not maintain its `length` field (unlike rbs_node_list_t), + // so count the entries by walking the list. + uint32_t count = 0; + for (rbs_hash_node_t *n = hash->head; n != NULL; n = n->next) { + count++; + } + w_u32(state, count); + + for (rbs_hash_node_t *n = hash->head; n != NULL; n = n->next) { + serialize_node(state, n->key); + serialize_node(state, n->value); + } +} + +// The node tag written ahead of every node. Tag 0 is reserved for NULL, tags +// 1..N are the nodes below (in the same order the Ruby schema is generated), +// and the final tag is `rbs_ast_symbol`, which is not a config.yml node. +#define RBS_SERIALIZE_TAG_SYMBOL <%= nodes.size + 1 %> + +static void serialize_node(rbs_serialize_state *state, rbs_node_t *instance) { + if (instance == NULL) { + w_u8(state, 0); + return; + } + + switch (instance->type) { + <%- nodes.each_with_index do |node, index| -%> + case <%= node.c_node_enum_name %>: { + w_u8(state, <%= index + 1 %>); + <%- case node.ruby_full_name -%> + <%- when "RBS::AST::Bool" -%> + w_u8(state, ((rbs_ast_bool_t *) instance)->value ? 1 : 0); + <%- when "RBS::AST::Integer" -%> + w_string(state, ((rbs_ast_integer_t *) instance)->string_representation); + <%- when "RBS::AST::String" -%> + w_string(state, ((rbs_ast_string_t *) instance)->string); + <%- when "RBS::Types::Record::FieldType" -%> + rbs_types_record_field_type_t *node = (rbs_types_record_field_type_t *) instance; + serialize_node(state, node->type); + w_u8(state, node->required ? 1 : 0); + <%- when "RBS::Signature" -%> + rbs_signature_t *node = (rbs_signature_t *) instance; + w_node_list(state, node->directives); + w_node_list(state, node->declarations); + <%- when "RBS::Namespace" -%> + rbs_namespace_t *node = (rbs_namespace_t *) instance; + w_node_list(state, node->path); + w_u8(state, node->absolute ? 1 : 0); + <%- when "RBS::TypeName" -%> + rbs_type_name_t *node = (rbs_type_name_t *) instance; + serialize_node(state, (rbs_node_t *) node->rbs_namespace); + serialize_node(state, (rbs_node_t *) node->name); + <%- else -%> + <%= node.c_type_name %> *node = (<%= node.c_type_name %> *) instance; + <%- if node.expose_location? -%> + w_loc_range(state, node->base.location); + <%- if node.locations -%> + <%- node.locations.each do |location_field| -%> + w_loc_range(state, node-><%= location_field.attribute_name %>); + <%- end -%> + <%- end -%> + <%- end -%> + <%- node.fields.each do |field| -%> + <%- case field.type.name -%> + <%- when "rbs_node_list" -%> + w_node_list(state, node-><%= field.c_name %>); + <%- when "rbs_hash" -%> + w_hash(state, node-><%= field.c_name %>); + <%- when "rbs_string" -%> + w_string(state, node-><%= field.c_name %>); + <%- when "bool" -%> + w_u8(state, node-><%= field.c_name %> ? 1 : 0); + <%- when "rbs_location_range" -%> + w_loc_range(state, node-><%= field.c_name %>); + <%- when "rbs_location_range_list" -%> + w_loc_range_list(state, node-><%= field.c_name %>); + <%- when "rbs_attr_ivar_name" -%> + w_attr_ivar_name(state, node-><%= field.c_name %>); + <%- else -%> + <%- if field.type.is_a?(RBS::Template::EnumType) -%> + w_u8(state, (uint8_t) node-><%= field.c_name %>); + <%- else -%> + serialize_node(state, (rbs_node_t *) node-><%= field.c_name %>); + <%- end -%> + <%- end -%> + <%- end -%> + <%- end -%> + return; + } + <%- end -%> + case RBS_AST_SYMBOL: { + w_u8(state, RBS_SERIALIZE_TAG_SYMBOL); + w_constant(state, ((rbs_ast_symbol_t *) instance)->constant_id); + return; + } + } + + RBS_ASSERT(false, "rbs_serialize_node: unknown node type: %d", instance->type); +} + +rbs_string_t rbs_serialize_node(rbs_allocator_t *allocator, rbs_constant_pool_t *constant_pool, rbs_node_t *node) { + rbs_serialize_state state = { + .allocator = allocator, + .constant_pool = constant_pool, + }; + rbs_buffer_init(allocator, &state.buffer); + + serialize_node(&state, node); + + return rbs_buffer_to_string(&state.buffer); +} + +rbs_string_t rbs_serialize_node_list(rbs_allocator_t *allocator, rbs_constant_pool_t *constant_pool, rbs_node_list_t *list) { + rbs_serialize_state state = { + .allocator = allocator, + .constant_pool = constant_pool, + }; + rbs_buffer_init(allocator, &state.buffer); + + w_node_list(&state, list); + + return rbs_buffer_to_string(&state.buffer); +} diff --git a/templates/template.rb b/templates/template.rb index b581ddbd74..c97052872f 100644 --- a/templates/template.rb +++ b/templates/template.rb @@ -281,14 +281,25 @@ def render(out_file) erb = read_template(template) extension = File.extname(filepath.gsub(".erb", "")) - heading = <<~HEADING - /*----------------------------------------------------------------------------*/ - /* This file is generated by the templates/template.rb script and should not */ - /* be modified manually. */ - /* To change the template see */ - /* #{filepath + " " * (74 - filepath.size) } */ - /*----------------------------------------------------------------------------*/ - HEADING + heading = + if extension == ".rb" + <<~HEADING + # frozen_string_literal: true + # + # This file is generated by the templates/template.rb script and should not be + # modified manually. To change the template see + # #{filepath} + HEADING + else + <<~HEADING + /*----------------------------------------------------------------------------*/ + /* This file is generated by the templates/template.rb script and should not */ + /* be modified manually. */ + /* To change the template see */ + /* #{filepath + " " * (74 - filepath.size) } */ + /*----------------------------------------------------------------------------*/ + HEADING + end write_to = File.expand_path("../#{out_file}", __dir__) contents = heading + "\n" + erb.result_with_hash(locals) diff --git a/test/rbs/ast/ruby/comment_block_test.rb b/test/rbs/ast/ruby/comment_block_test.rb index 5ce1796855..66d4b51b78 100644 --- a/test/rbs/ast/ruby/comment_block_test.rb +++ b/test/rbs/ast/ruby/comment_block_test.rb @@ -60,6 +60,9 @@ def test__buffer__multi_line_prefix_header_line end def test_build + omit_on_truffle_ruby! "`Prism::Location#start_line_slice` returns `nil` on TruffleRuby's prism" + omit_on_jruby! "`Prism::Location#start_line_slice` returns `nil` on JRuby's prism" + buffer, comments = parse_comments(<<~RUBY) # Comment1 # Comment2 @@ -188,6 +191,9 @@ def test_each_paragraph_colon end def test_trailing_annotation + omit_on_truffle_ruby! "`Prism::Location#start_line_slice` returns `nil` on TruffleRuby's prism" + omit_on_jruby! "`Prism::Location#start_line_slice` returns `nil` on JRuby's prism" + buffer, comments = parse_comments(<<~RUBY) foo #: String @@ -218,6 +224,9 @@ def test_trailing_annotation end def test_trailing_annotation_type_application + omit_on_truffle_ruby! "`Prism::Location#start_line_slice` returns `nil` on TruffleRuby's prism" + omit_on_jruby! "`Prism::Location#start_line_slice` returns `nil` on JRuby's prism" + buffer, comments = parse_comments(<<~RUBY) foo #[String] diff --git a/test/rbs/cli_test.rb b/test/rbs/cli_test.rb index a9becac114..3c70ffa203 100644 --- a/test/rbs/cli_test.rb +++ b/test/rbs/cli_test.rb @@ -891,6 +891,9 @@ def test_parse_method_type end def test_prototype_no_parser + omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" + omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" + Dir.mktmpdir do |dir| with_cli do |cli| def cli.has_parser? @@ -907,6 +910,9 @@ def cli.has_parser? end def test_prototype_batch + omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" + omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" + Dir.mktmpdir do |dir| dir = Pathname(dir) @@ -968,6 +974,9 @@ module C end def test_prototype_batch_outer + omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" + omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" + Dir.mktmpdir do |dir| dir = Pathname(dir) @@ -994,6 +1003,9 @@ module A end def test_prototype_batch_syntax_error + omit_on_truffle_ruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on TruffleRuby" + omit_on_jruby! "`rbs prototype` requires `RubyVM::AbstractSyntaxTree`, which is not available on JRuby" + Dir.mktmpdir do |dir| dir = Pathname(dir) @@ -1042,6 +1054,9 @@ def test_prototype__runtime__todo def test_test + omit_on_truffle_ruby! "`rbs test` relies on `TracePoint` `:end` event, which is not supported on TruffleRuby" + omit_on_jruby! "`rbs test` relies on `TracePoint` `:end` event, which is not supported on JRuby" + Dir.mktmpdir do |dir| dir = Pathname(dir) dir.join('foo.rbs').write(<<~RBS) @@ -1068,6 +1083,8 @@ def foo: () -> void end def test_collection_install + omit_on_jruby! "`rbs collection install` runs `bundle install`, which builds native gem extensions that do not compile on JRuby" + Dir.mktmpdir do |dir| Dir.chdir(dir) do dir = Pathname(dir) @@ -1134,6 +1151,8 @@ def test_collection_install_frozen end def test_collection_update + omit_on_jruby! "`rbs collection update` runs `bundle install`, which builds native gem extensions that do not compile on JRuby" + Dir.mktmpdir do |dir| Dir.chdir(dir) do dir = Pathname(dir) @@ -1157,6 +1176,8 @@ def test_collection_update end def test_collection_install_gemspec + omit_on_jruby! "`rbs collection install` runs `bundle install`, which builds native gem extensions that do not compile on JRuby" + Dir.mktmpdir do |dir| Dir.chdir(dir) do dir = Pathname(dir) diff --git a/test/rbs/inline_annotation_parsing_test.rb b/test/rbs/inline_annotation_parsing_test.rb index b76c8533c3..266db36b8e 100644 --- a/test/rbs/inline_annotation_parsing_test.rb +++ b/test/rbs/inline_annotation_parsing_test.rb @@ -559,4 +559,28 @@ def test_error__module_self Parser.parse_inline_leading_annotation("@rbs module-self: foo", 0...) end end + + def test_parse__trailing_assertion__multibyte_offset + buffer = Buffer.new(name: Pathname("test.rb"), content: "日本語\n: String") + + Parser.parse_inline_trailing_annotation(buffer, 4...12).tap do |annot| + assert_instance_of AST::Ruby::Annotations::NodeTypeAssertion, annot + assert_equal ": String", annot.location.source + assert_equal ":", annot.prefix_location.source + assert_equal "String", annot.type.location.source + end + end + + def test_parse__leading_annotation__multibyte_offset + buffer = Buffer.new(name: Pathname("test.rb"), content: "日本語のコメント\n@rbs return: String -- 戻り値の説明") + + Parser.parse_inline_leading_annotation(buffer, 9...).tap do |annot| + assert_instance_of AST::Ruby::Annotations::ReturnTypeAnnotation, annot + assert_equal "@rbs return: String -- 戻り値の説明", annot.location.source + assert_equal "return", annot.return_location.source + assert_equal ":", annot.colon_location.source + assert_equal "String", annot.return_type.location.source + assert_equal "-- 戻り値の説明", annot.comment_location.source + end + end end diff --git a/test/rbs/location_test.rb b/test/rbs/location_test.rb index 520028ea2d..391a0e66f2 100644 --- a/test/rbs/location_test.rb +++ b/test/rbs/location_test.rb @@ -162,6 +162,8 @@ def test_sub_buffer_local_location end def test_gc_compaction + omit_on_truffle_ruby! "`GC.compact` is not implemented on TruffleRuby" + GC.disable buffer = buffer() locations = 10.times.map do |i| diff --git a/test/rbs/namespace_test.rb b/test/rbs/namespace_test.rb new file mode 100644 index 0000000000..fc520739e5 --- /dev/null +++ b/test/rbs/namespace_test.rb @@ -0,0 +1,57 @@ +require "test_helper" + +class RBS::NamespaceTest < Test::Unit::TestCase + Namespace = RBS::Namespace + + def test_intern_returns_same_instance_for_equal_arguments + a = Namespace[[:Foo, :Bar], true] + b = Namespace[[:Foo, :Bar], true] + + assert_same a, b + end + + def test_intern_distinguishes_absolute_and_relative + absolute = Namespace[[:Foo], true] + relative = Namespace[[:Foo], false] + + refute_same absolute, relative + assert_equal true, absolute.absolute? + assert_equal false, relative.absolute? + end + + def test_intern_freezes_internal_path + ns = Namespace[[:Foo, :Bar], true] + + assert_predicate ns.path, :frozen? + end + + def test_intern_is_isolated_from_caller_mutation + path = [:Foo] + ns = Namespace[path, true] + + path << :Bar + + assert_equal [:Foo], ns.path + assert_same ns, Namespace[[:Foo], true] + end + + def test_intern_coerces_absolute_truthiness + truthy = Namespace[[:Foo], "yes"] + plain = Namespace[[:Foo], true] + + assert_same truthy, plain + end + + def test_intern_equals_uninterned_new + interned = Namespace[[:Foo, :Bar], true] + fresh = Namespace.new(path: [:Foo, :Bar], absolute: true) + + assert_equal interned, fresh + assert_equal interned.hash, fresh.hash + end + + def test_empty_and_root_are_interned + assert_same Namespace.empty, Namespace[[], false] + assert_same Namespace.root, Namespace[[], true] + end +end diff --git a/test/rbs/node_usage_test.rb b/test/rbs/node_usage_test.rb index bdc933c49b..7f20677afc 100644 --- a/test/rbs/node_usage_test.rb +++ b/test/rbs/node_usage_test.rb @@ -8,6 +8,9 @@ def parse(string) end def test_conditional + omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" + omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" + NodeUsage.new(parse(<<~RB)) def block yield diff --git a/test/rbs/parser_test.rb b/test/rbs/parser_test.rb index ea160cc9d5..f78a8597d8 100644 --- a/test/rbs/parser_test.rb +++ b/test/rbs/parser_test.rb @@ -1,4 +1,5 @@ require "test_helper" +require "timeout" class RBS::ParserTest < Test::Unit::TestCase def buffer(source) @@ -1028,4 +1029,49 @@ class Foo[T < Integer] < Bar # Comment assert_equal [:tTRIVIA, "\n", 56...57], tokens.shift.then { |t| [t[0], t[1].source, t[1].range] } assert_equal [:pEOF, '', 57...57], tokens.shift.then { |t| [t[0], t[1].source, t[1].range] } end + + def test_invalid_position_range_raises + # Regression: start_pos > end_pos used to cause an infinite loop in the lexer. + assert_raises(ArgumentError) do + RBS::Parser._parse_signature(buffer(""), 1, 0) + end + end + + def test_invalid_byte_range_in_parse_type_raises + # Regression: parse_type's byte_range: keyword reaches _parse_type directly, + # which used to hang on reversed ranges. + assert_raises(ArgumentError) do + RBS::Parser.parse_type("", byte_range: 1..0) + end + end + + def test_invalid_utf8_byte_in_comment_does_not_hang + omit_on_truffle_ruby! "The C extension does not raise `RBS::ParsingError` for an invalid UTF-8 byte on TruffleRuby" + + # Regression: invalid UTF-8 byte in a comment used to loop forever in the lexer. + source = "# \xC2".dup.force_encoding(Encoding::UTF_8) + assert_raises(RBS::ParsingError) do + RBS::Parser._parse_signature(buffer(source), 0, source.bytesize) + end + end + + def test_invalid_utf8_byte_at_top_level_raises + omit_on_truffle_ruby! "The C extension does not raise `RBS::ParsingError` for an invalid UTF-8 byte on TruffleRuby" + + # Regression: invalid UTF-8 byte at top level used to trip RBS_ASSERT in the C extension. + source = "\xFF".dup.force_encoding(Encoding::UTF_8) + assert_raises(RBS::ParsingError) do + RBS::Parser._parse_signature(buffer(source), 0, source.bytesize) + end + end + + def test_utf8_replacement_character_in_comment_parses + # A genuine U+FFFD (REPLACEMENT CHARACTER) is a valid 3-byte UTF-8 sequence + # ("\xEF\xBF\xBD") that decodes to the multibyte dummy code point, not the + # sentinel that marks an invalid byte. A comment containing it must parse fine. + source = "# \u{FFFD}\ntype x = untyped\n".dup.force_encoding(Encoding::UTF_8) + _, decls = RBS::Parser._parse_signature(buffer(source), 0, source.bytesize) + assert_equal 1, decls.size + assert_instance_of RBS::AST::Declarations::TypeAlias, decls[0] + end end diff --git a/test/rbs/rb_prototype_test.rb b/test/rbs/rb_prototype_test.rb index 2722347ece..d628bec0df 100644 --- a/test/rbs/rb_prototype_test.rb +++ b/test/rbs/rb_prototype_test.rb @@ -1,6 +1,9 @@ require "test_helper" class RBS::RbPrototypeTest < Test::Unit::TestCase + omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" + omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" + RB = RBS::Prototype::RB include TestHelper diff --git a/test/rbs/rbi_prototype_test.rb b/test/rbs/rbi_prototype_test.rb index 8f0107a0a9..c65cc535a7 100644 --- a/test/rbs/rbi_prototype_test.rb +++ b/test/rbs/rbi_prototype_test.rb @@ -1,6 +1,9 @@ require "test_helper" class RBS::RbiPrototypeTest < Test::Unit::TestCase + omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" + omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" + RBI = RBS::Prototype::RBI include TestHelper @@ -67,9 +70,8 @@ module Bar assert_write parser.decls, <<-EOF module Foo -end - -module Foo::Bar + module Bar + end end EOF end @@ -88,9 +90,8 @@ module ::Bar assert_write parser.decls, <<-EOF module Foo -end - -module Bar + module ::Bar + end end EOF end @@ -109,11 +110,10 @@ module Foo assert_write parser.decls, <<-EOF module Foo -end + ABBR_DAYNAMES: Array -Foo::ABBR_DAYNAMES: Array - -Foo::ABBR_MONTHNAMES: Integer + ABBR_MONTHNAMES: Integer +end EOF end @@ -547,6 +547,296 @@ class Dir[out X, in Y, Z] EOF end + def test_nested_declarations_preserve_lexical_resolution + parser = RBI.new + + parser.parse <<-EOF +module Demo + class Parent; end + module Helpers; end + class Value; end + + class Child < Parent + include Helpers + + sig { params(value: Value).returns(Value) } + def convert(value); end + end +end + EOF + + assert_write parser.decls, <<-EOF +module Demo + class Parent + end + + module Helpers + end + + class Value + end + + class Child < Parent + include Helpers + + def convert: (Value value) -> Value + end +end + EOF + end + + def test_nested_constant + parser = RBI.new + + parser.parse <<-EOF +module Demo + module Modes + VALUE = T.let(:value, Symbol) + end +end + EOF + + assert_write parser.decls, <<-EOF +module Demo + module Modes + VALUE: Symbol + end +end + EOF + end + + def test_ignores_t_helpers + parser = RBI.new + + parser.parse <<-EOF +module Factory + extend T::Helpers + extend OtherHelpers +end + EOF + + assert_write parser.decls, <<-EOF +module Factory + extend OtherHelpers +end + EOF + end + + def test_t_class_falls_back_to_untyped + parser = RBI.new + + parser.parse <<-EOF +module Factory + sig do + type_parameters(:Config) + .params(config_class: T::Class[T.type_parameter(:Config)]) + .returns(T.type_parameter(:Config)) + end + def make(config_class); end +end + EOF + + assert_write parser.decls, <<-EOF +module Factory + def make: [Config] (untyped config_class) -> Config +end + EOF + end + + def test_singleton_class_method + parser = RBI.new + + parser.parse <<-EOF +class Registry + class << self + sig { returns(T.attached_class) } + def build; end + end +end + EOF + + assert_write parser.decls, <<-EOF +class Registry + def self.build: () -> instance +end + EOF + end + + def test_typed_attribute_consumes_signature + parser = RBI.new + + parser.parse <<-EOF +class Cache + sig { returns(T.nilable(Integer)) } + attr_reader :size + + sig { returns(String) } + attr_accessor :name + + sig { params(value: Integer).void } + attr_writer :count + + sig { params(size: T.nilable(Integer)).void } + def initialize(size: nil); end +end + EOF + + assert_write parser.decls, <<-EOF +class Cache + attr_reader size: Integer? + + attr_accessor name: String + + attr_writer count: Integer + + def initialize: (?size: Integer? size) -> void +end + EOF + end + + def test_method_visibility + parser = RBI.new + + parser.parse <<-EOF +module Factory + private + + sig { void } + def helper; end + + public + + sig { void } + def make; end +end + EOF + + assert_write parser.decls, <<-EOF +module Factory + private + + def helper: () -> void + + public + + def make: () -> void +end + EOF + end + + def test_singleton_method_visibility + parser = RBI.new + + parser.parse <<-EOF +class Registry + private + + sig { void } + def helper; end + + sig { void } + def self.build; end + + sig { void } + def setup; end + + class << self + private + + sig { void } + def internal; end + + sig { returns(Integer) } + attr_reader :count + end +end + EOF + + assert_write parser.decls, <<-EOF +class Registry + private + + def helper: () -> void + + def self.build: () -> void + + def setup: () -> void + + private def self.internal: () -> void + + private attr_reader self.count: Integer +end + EOF + end + + def test_generated_reproduction_can_be_loaded + parser = RBI.new + + parser.parse <<-EOF +module Demo + class Parent; end + module Helpers; end + + class Child < Parent + include Helpers + end + + module Modes + VALUE = T.let(:value, Symbol) + end + + class Registry + class << self + sig { returns(T.attached_class) } + def build; end + end + end + + class Cache + sig { returns(T.nilable(Integer)) } + attr_reader :size + + sig { params(size: T.nilable(Integer)).void } + def initialize(size: nil); end + end + + module Factory + extend T::Helpers + + sig do + type_parameters(:Config) + .params(config_class: T::Class[T.type_parameter(:Config)]) + .returns(T.type_parameter(:Config)) + end + def make(config_class); end + + private + + sig { void } + def helper; end + end +end + EOF + + out = StringIO.new + RBS::Writer.new(out: out).write(parser.decls) + refute_match(/\bT::/, out.string) + + SignatureManager.new do |manager| + manager.add_file("repro.rbs", out.string) + manager.build do |env| + builder = RBS::DefinitionBuilder.new(env: env) + + ["::Demo::Child", "::Demo::Registry", "::Demo::Cache", "::Demo::Factory"].each do |name| + builder.build_instance(type_name(name)) + builder.build_singleton(type_name(name)) + end + + assert_include env.constant_decls.keys, type_name("::Demo::Modes::VALUE") + end + end + end + def test_masgn parser = RBI.new @@ -558,13 +848,12 @@ class Test assert_write parser.decls, <<-EOF class Test -end + A: untyped -Test::A: untyped + B: untyped -Test::B: untyped - -Test::C: untyped + C: untyped +end EOF end end diff --git a/test/rbs/rdoc/rbs_parser_test.rb b/test/rbs/rdoc/rbs_parser_test.rb deleted file mode 100644 index a92f10b356..0000000000 --- a/test/rbs/rdoc/rbs_parser_test.rb +++ /dev/null @@ -1,391 +0,0 @@ -require "test_helper" - -require "rdoc_plugin/parser" - -class RDocPluginParserTest < Test::Unit::TestCase - def parser(content) - top_level = RDoc::TopLevel.new("a.rbs") - top_level.store = RDoc::Store.new(RDoc::Options.new) - - RBS::RDocPlugin::Parser.new(top_level, content) - end - - def test_class_decl_1 - parser = parser(<<~RBS) -class A -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - - assert_instance_of RDoc::NormalClass, klass - assert_equal "A", klass.full_name - assert_equal "A", klass.name - assert_equal "", klass.comment - assert_equal "Object", klass.superclass - end - - def test_class_decl_2 - parser = parser(<<~RBS) -# This is class A -class A[Store] < StringBuilder[Store] -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - - assert_instance_of RDoc::NormalClass, klass - assert_equal "A", klass.full_name - assert_equal "A", klass.name - assert_equal "This is class A", klass.comment.text - assert_equal "StringBuilder", klass.superclass - end - - def test_module_decl_1 - parser = parser(<<~RBS) -module A -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - - assert_instance_of RDoc::NormalModule, klass - assert_equal "A", klass.full_name - assert_equal "A", klass.name - assert_equal "", klass.comment - end - - def test_module_decl_2 - parser = parser(<<~RBS) -# Test comment for module A -module A[Element] : _Each[Element] -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - - assert_instance_of RDoc::NormalModule, klass - assert_equal "A", klass.full_name - assert_equal "A", klass.name - assert_equal "Test comment for module A", klass.comment.text - end - - def test_instance_method_1 - parser = parser(<<~RBS) -class A - def foo: (Integer) -> void -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - method = klass.method_list.find { |m| m.name == "foo" } - - assert_instance_of RDoc::AnyMethod, method - assert_equal "", method.comment - assert_equal "foo(Integer) -> void", method.call_seq - end - - def test_instance_method_with_block - parser = parser(<<~RBS) -class A - def foo: (Integer) { (String) -> void } -> void -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - method = klass.method_list.find { |m| m.name == "foo" } - - assert_instance_of RDoc::AnyMethod, method - assert_equal "", method.comment - assert_equal "foo(Integer) { (String) -> void } -> void", method.call_seq - end - - def test_instance_method_generic - parser = parser(<<~RBS) -class A - def foo: [A] (Integer) { (String) -> A } -> A -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - method = klass.method_list.find { |m| m.name == "foo" } - - assert_instance_of RDoc::AnyMethod, method - assert_equal "", method.comment - assert_equal "foo[A] (Integer) { (String) -> A } -> A", method.call_seq - end - - def test_instance_method_comment_and_tokens - parser = parser(<<~RBS) -class A - # Added comment for foo - def foo: [A] (Integer) { (String) -> A } -> A -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - method = klass.method_list.find { |m| m.name == "foo" } - - assert_instance_of RDoc::AnyMethod, method - assert_equal "Added comment for foo", method.comment.text - assert_equal "foo[A] (Integer) { (String) -> A } -> A", method.call_seq - assert_equal "# File a.rbs, line(s) 3:3\n" + "def foo: [A] (Integer) { (String) -> A } -> A", method.tokens_to_s - end - - def test_constant_decl_1 - parser = parser(<<~RBS) -class A - CONSTANT: Integer -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - constant = klass.constants.first - - assert_instance_of RDoc::Constant, constant - assert_equal "CONSTANT", constant.name - assert_equal "Integer", constant.value - assert_equal "", constant.comment - end - - def test_constant_decl_2 - parser = parser(<<~RBS) -class A - # Constant comment test - CONSTANT: ("Literal" | "Union check") -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - constant = klass.constants.first - - assert_instance_of RDoc::Constant, constant - assert_equal "CONSTANT", constant.name - assert_equal "\"Literal\" | \"Union check\"", constant.value - assert_equal "Constant comment test", constant.comment.text - end - - def test_method_alias_decl_1 - parser = parser(<<~RBS) -class A - def foo: () -> void - alias bar foo -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - method = klass.method_list.find { |m| m.name == "bar" } - - assert_instance_of RDoc::AnyMethod, method - assert_equal "bar", method.name - assert_instance_of RDoc::AnyMethod, method.is_alias_for - assert_equal "foo", method.is_alias_for.name - assert_nil method.is_alias_for.is_alias_for - end - - def test_method_alias_decl_2 - parser = parser(<<~RBS) -class A - def foo: () -> void - alias bar foo - alias baz bar - alias foo dam -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - method = klass.method_list.find { |m| m.name == "baz" } - - assert_instance_of RDoc::AnyMethod, method - assert_equal "baz", method.name - assert_instance_of RDoc::AnyMethod, method.is_alias_for - assert_equal "bar", method.is_alias_for.name - assert_instance_of RDoc::AnyMethod, method.is_alias_for.is_alias_for - assert_equal "foo", method.is_alias_for.is_alias_for.name - - assert_instance_of RDoc::Alias, klass.external_aliases.first - assert_equal "foo", klass.external_aliases.first.name - assert_equal "dam", klass.external_aliases.first.old_name - end - - def test_attr_decl_1 - parser = parser(<<~RBS) -class A - attr_reader foo: Integer - attr_writer bar: Float - attr_accessor dam: String -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - attr_r = klass.attributes[0] - attr_w = klass.attributes[1] - attr_a = klass.attributes[2] - - assert_instance_of RDoc::Attr, attr_r - assert_equal "foo", attr_r.name - assert_equal "R", attr_r.rw - assert_equal "", attr_r.comment - - assert_instance_of RDoc::Attr, attr_w - assert_equal "bar", attr_w.name - assert_equal "W", attr_w.rw - assert_equal "", attr_w.comment - - assert_instance_of RDoc::Attr, attr_a - assert_equal "dam", attr_a.name - assert_equal "RW", attr_a.rw - assert_equal "", attr_a.comment - end - - def test_attr_decl_2 - parser = parser(<<~RBS) -class A - # Comment 1 - attr_reader foo: Integer - # Comment 2 - attr_writer bar: Float - # Comment 3 - attr_accessor dam: String -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A") - attr_r = klass.attributes[0] - attr_w = klass.attributes[1] - attr_a = klass.attributes[2] - - assert_instance_of RDoc::Attr, attr_r - assert_equal "foo", attr_r.name - assert_equal "R", attr_r.rw - assert_equal "Comment 1", attr_r.comment.text - - assert_instance_of RDoc::Attr, attr_w - assert_equal "bar", attr_w.name - assert_equal "W", attr_w.rw - assert_equal "Comment 2", attr_w.comment.text - - assert_instance_of RDoc::Attr, attr_a - assert_equal "dam", attr_a.name - assert_equal "RW", attr_a.rw - assert_equal "Comment 3", attr_a.comment.text - end - - def test_include_decl_1 - parser = parser(<<~RBS) -module D -end -class A - module B - end - class C - include B - # Test comment - include D - end -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A::C") - rdoc_include = klass.includes.first - assert_instance_of RDoc::Include, rdoc_include - assert_equal "A::B", rdoc_include.name - assert_equal "", rdoc_include.comment - - rdoc_include = klass.includes[1] - assert_instance_of RDoc::Include, rdoc_include - assert_equal "D", rdoc_include.name - assert_equal "Test comment", rdoc_include.comment.text - end - - def test_extend_decl_1 - parser = parser(<<~RBS) -module D -end -class A - module B - end - class C - extend B - # Test comment - extend D - end -end -RBS - - parser.scan() - - top_level = parser.top_level - - klass = top_level.find_class_or_module("A::C") - rdoc_extend = klass.extends.first - assert_instance_of RDoc::Extend, rdoc_extend - assert_equal "A::B", rdoc_extend.name - assert_equal "", rdoc_extend.comment - - rdoc_extend = klass.extends[1] - assert_instance_of RDoc::Extend, rdoc_extend - assert_equal "D", rdoc_extend.name - assert_equal "Test comment", rdoc_extend.comment.text - end -end diff --git a/test/rbs/runtime_prototype_test.rb b/test/rbs/runtime_prototype_test.rb index 4ec7612b4a..24efac0cda 100644 --- a/test/rbs/runtime_prototype_test.rb +++ b/test/rbs/runtime_prototype_test.rb @@ -1,6 +1,9 @@ require "test_helper" class RBS::RuntimePrototypeTest < Test::Unit::TestCase + omit_on_truffle_ruby! "`RubyVM::AbstractSyntaxTree` is not available on TruffleRuby" + omit_on_jruby! "`RubyVM::AbstractSyntaxTree` is not available on JRuby" + Runtime = RBS::Prototype::Runtime DefinitionBuilder = RBS::DefinitionBuilder diff --git a/test/rbs/test/runtime_test_test.rb b/test/rbs/test/runtime_test_test.rb index 89b75f8aaf..ad8b969c30 100644 --- a/test/rbs/test/runtime_test_test.rb +++ b/test/rbs/test/runtime_test_test.rb @@ -5,6 +5,9 @@ return unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') class RBS::Test::RuntimeTestTest < Test::Unit::TestCase + omit_on_truffle_ruby! "`rbs test` relies on `TracePoint` `:end` event, which is not supported on TruffleRuby" + omit_on_jruby! "`rbs test` relies on `TracePoint` `:end` event, which is not supported on JRuby" + include TestHelper def test_runtime_success diff --git a/test/rbs/test/type_check_test.rb b/test/rbs/test/type_check_test.rb index 20c40dd76b..a8e2824adb 100644 --- a/test/rbs/test/type_check_test.rb +++ b/test/rbs/test/type_check_test.rb @@ -10,6 +10,9 @@ class RBS::Test::TypeCheckTest < Test::Unit::TestCase include RBS def test_type_check + omit_on_truffle_ruby! "Type checking a block-less `loop` Enumerator behaves differently on TruffleRuby" + omit_on_jruby! "Type checking a block-less `loop` Enumerator behaves differently on JRuby" + SignatureManager.new do |manager| manager.files[Pathname("foo.rbs")] = <, 0, "expected to find bundled RBS files" + + paths.each do |path| + source = File.read(path, encoding: "UTF-8") + _buffer, _directives, declarations = RBS::Parser.parse_signature(source) + assert_not_nil declarations, "failed to parse #{path}" + end + end + + def test_parse_signature_structure + _buffer, _directives, declarations = RBS::Parser.parse_signature(<<~RBS) + class Foo < Bar + attr_reader name: String + def greet: (String name) -> String + end + RBS + + decl = declarations[0] + assert_instance_of RBS::AST::Declarations::Class, decl + assert_equal "Foo", decl.name.to_s + assert_equal "Bar", decl.super_class&.name&.to_s + assert_equal [:name, :greet], decl.members.map { |member| member.respond_to?(:name) ? member.name : nil } + assert_equal 1, decl.location.start_line + end + + def test_parse_type + assert_equal "Hash[Symbol, Array[Integer]]", RBS::Parser.parse_type("Hash[Symbol, Array[Integer]]").to_s + assert_equal "^(Integer, ?String) { () -> void } -> bool", RBS::Parser.parse_type("^(Integer, ?String) { () -> void } -> bool").to_s + assert_equal "A | B", RBS::Parser.parse_type("A | B", variables: [:A, :B]).to_s + end + + def test_parse_method_type + assert_equal "[T] (T, ?Integer) { (T) -> void } -> T", RBS::Parser.parse_method_type("[T] (T, ?Integer) { (T) -> void } -> T").to_s + end + + def test_parse_type_params + params = RBS::Parser.parse_type_params("[T < Comparable, unchecked out U]") + assert_equal [:T, :U], params.map(&:name) + assert_equal "Comparable", params[0].upper_bound&.name&.to_s + assert_equal :covariant, params[1].variance + end + + def test_lex + types = RBS::Parser.lex("class Foo\nend\n").value.map(&:type) + assert_include types, :kCLASS + assert_equal :pEOF, types.last + end + + def test_parse_error_raises_parsing_error + error = assert_raises(RBS::ParsingError) do + RBS::Parser.parse_signature("class 123 Broken end") + end + assert_not_nil error.location + assert_equal "tINTEGER", error.token_type + end + end + end +end diff --git a/test/rbs/wasm/serialization_test.rb b/test/rbs/wasm/serialization_test.rb new file mode 100644 index 0000000000..ccd4b99a0f --- /dev/null +++ b/test/rbs/wasm/serialization_test.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +require "test_helper" +require "rbs/wasm/deserializer" + +# Verifies that the binary serialization produced by `rbs_serialize_node` +# (src/serialize.c) round-trips back into exactly the same AST objects that the +# C extension builds directly via ast_translation.c. +# +# The parser is run twice over the same buffer: once through the normal +# C -> Ruby translation, and once through serialize -> deserialize. The two +# results must be deeply identical, down to locations and string encodings. This +# is what gives us confidence that the same format, produced inside WebAssembly, +# will rebuild correct objects on JRuby. +class RBS::WASM::SerializationTest < Test::Unit::TestCase + # The round-trip is driven by the C extension's `_parse_*_to_bytes`, which only + # exists on CRuby. JRuby's end-to-end coverage lives in jruby_parser_test.rb. + omit_on_jruby! "Uses the C extension's _parse_*_to_bytes helpers" + + ROOT = File.expand_path("../../..", __dir__) + + def buffer(source) + RBS::Buffer.new(content: source, name: "test.rbs") + end + + def assert_round_trips(buf) + directives, decls = RBS::Parser._parse_signature(buf, 0, buf.content.bytesize) + bytes = RBS::Parser._parse_signature_to_bytes(buf, 0, buf.content.bytesize) + actual = RBS::WASM::Deserializer.deserialize(bytes, buf) + + diff = ast_diff([directives, decls], actual) + assert_nil diff, "round-trip mismatch in #{buf.name}: #{diff}" + end + + def test_signature_round_trip_for_bundled_rbs + paths = Dir.glob(File.join(ROOT, "{core,stdlib,sig}/**/*.rbs")).sort + assert_operator paths.size, :>, 0, "expected to find bundled RBS files" + + paths.each do |path| + source = File.read(path) + assert_round_trips(RBS::Buffer.new(content: source, name: path)) + end + end + + def test_signature_round_trip_for_features + sources = [ + "class Foo end", + "class Foo < Bar end", + "class Foo[A, out B, unchecked in C < Comparable[A]] end", + "module M : Comparable, _Each[Integer] end", + <<~RBS, + # A documented class. + class Account + @balance: Integer + self.@registry: Hash[Symbol, Account] + @@count: Integer + + attr_reader name: String + attr_accessor age (@years): Integer + attr_writer secret (): String + + public + def deposit: (Integer amount) -> void + | (Float) -> void + private + def self.find: (Symbol) -> Account? + alias credit deposit + include Comparable + extend ClassMethods + prepend Logging + end + RBS + "type t[T] = [T, t[T]?] | { value: T, ?next: t[T] } | ^(T) { () -> void } -> bool", + "type lit = 1 | -2 | :sym | \"str\" | true | false | nil", + "interface _Each[A] def each: () { (A) -> void } -> void end", + "$global: Integer\nCONST: String\nFoo::Bar: bool", + "class A = B\nmodule M = N", + "use Foo::Bar, Baz::*, Qux::Quux as Q\nclass A end", + "# resolve-type-names: false\nclass A end", + ] + + sources.each { |source| assert_round_trips(buffer(source)) } + end + + def test_type_round_trip + types = [ + "Integer", "::Foo::Bar::Baz", "Array[Integer]", "Integer | String | nil", + "(Integer & Comparable)", "Integer?", "[Integer, String, bool]", + "{ name: String, ?age: Integer }", "^(Integer, ?String) { () -> void } -> bool", + "^() [self: Foo] -> void", "singleton(String)", "self", "instance", "class", + "void", "untyped", "bool", "top", "bot", "nil", + "1", "-42", ":symbol", '"string"', "true", "false", + "123456789012345678901234567890", "Hash[Symbol, Array[Integer]]", "_Each[String]", + ] + + types.each do |source| + buf = buffer(source) + expected = RBS::Parser._parse_type(buf, 0, source.bytesize, nil, true, true, true, true) + bytes = RBS::Parser._parse_type_to_bytes(buf, 0, source.bytesize, nil, true, true, true, true) + actual = RBS::WASM::Deserializer.deserialize(bytes, buf) + + assert_nil ast_diff(expected, actual), "type round-trip mismatch for #{source.inspect}" + end + end + + def test_method_type_round_trip + method_types = [ + "() -> void", + "(Integer) -> String", + "[T] (T) -> T", + "(Integer, ?String, *Symbol, foo: bool, ?bar: Integer, **untyped) -> void", + "() { (Integer) -> void } -> bool", + "() ?{ () -> void } -> void", + "[A, B < Comparable[A]] (A) -> B", + ] + + method_types.each do |source| + buf = buffer(source) + expected = RBS::Parser._parse_method_type(buf, 0, source.bytesize, nil, true) + bytes = RBS::Parser._parse_method_type_to_bytes(buf, 0, source.bytesize, nil, true) + actual = RBS::WASM::Deserializer.deserialize(bytes, buf) + + assert_nil ast_diff(expected, actual), "method type round-trip mismatch for #{source.inspect}" + end + end + + private + + # Returns nil when the two trees are deeply identical, or a String describing + # the first difference found. This is stricter than RBS object `==` (which + # ignores locations and comments) and also checks string encodings, so it + # catches anything the serialization could get subtly wrong. + def ast_diff(a, b, path = "") + return nil if a.equal?(b) + + case a + when nil, true, false, Symbol, Integer, Float + a == b ? nil : "#{path}: #{a.inspect} != #{b.inspect}" + when String + if a == b && a.encoding == b.encoding + nil + else + "#{path}: #{a.inspect} (#{a.encoding}) != #{b.inspect} (#{b.encoding})" + end + when Array + return "#{path}: expected Array, got #{b.class}" unless b.is_a?(Array) + return "#{path}: size #{a.size} != #{b.size}" unless a.size == b.size + + a.each_index do |i| + diff = ast_diff(a[i], b[i], "#{path}[#{i}]") + return diff if diff + end + nil + when Hash + return "#{path}: expected Hash, got #{b.class}" unless b.is_a?(Hash) + return "#{path}: size #{a.size} != #{b.size}" unless a.size == b.size + + a.each do |key, value| + return "#{path}: missing key #{key.inspect}" unless b.key?(key) + + diff = ast_diff(value, b[key], "#{path}{#{key.inspect}}") + return diff if diff + end + nil + when RBS::Location + return "#{path}: expected Location, got #{b.class}" unless b.is_a?(RBS::Location) + a == b ? nil : "#{path}: location #{a} != #{b}" + when RBS::Buffer + a.name == b.name ? nil : "#{path}: buffer #{a.name.inspect} != #{b.name.inspect}" + else + return "#{path}: class #{a.class} != #{b.class}" unless a.class == b.class + + a_ivars = a.instance_variables.sort + unless a_ivars == b.instance_variables.sort + return "#{path}: ivars #{a_ivars} != #{b.instance_variables.sort}" + end + + a_ivars.each do |ivar| + diff = ast_diff(a.instance_variable_get(ivar), b.instance_variable_get(ivar), "#{path}.#{ivar}") + return diff if diff + end + nil + end + end +end diff --git a/test/stdlib/ARGF_test.rb b/test/stdlib/ARGF_test.rb index 3a9cad8f7b..22d0d77422 100644 --- a/test/stdlib/ARGF_test.rb +++ b/test/stdlib/ARGF_test.rb @@ -283,8 +283,12 @@ def test_read ARGF.class.new(temp_file), :read assert_send_type "(::int length) -> ::String", ARGF.class.new(temp_file), :read, 1 + assert_send_type "(::int length, nil) -> ::String", + ARGF.class.new(temp_file), :read, 1, nil assert_send_type "(::int length, ::string outbuf) -> ::String", ARGF.class.new(temp_file), :read, 1, +"" + assert_send_type "(nil, nil) -> ::String", + ARGF.class.new(temp_file), :read, nil, nil assert_send_type "(::int length) -> nil", ARGF.class.new(Tempfile.new), :read, 1 end @@ -292,6 +296,8 @@ def test_read def test_read_nonblock assert_send_type "(::int maxlen) -> ::String", ARGF.class.new(temp_file), :read_nonblock, 1 + assert_send_type "(::int maxlen, nil) -> ::String", + ARGF.class.new(temp_file), :read_nonblock, 1, nil assert_send_type "(::int maxlen, ::string buf) -> ::String", ARGF.class.new(temp_file), :read_nonblock, 1, +"" end @@ -309,6 +315,8 @@ def test_readchar def test_readpartial assert_send_type "(::int maxlen) -> ::String", ARGF.class.new(temp_file), :readpartial, 1 + assert_send_type "(::int maxlen, nil) -> ::String", + ARGF.class.new(temp_file), :readpartial, 1, nil assert_send_type "(::int maxlen, ::string buf) -> ::String", ARGF.class.new(temp_file), :readpartial, 1, +"" end diff --git a/test/stdlib/Array_test.rb b/test/stdlib/Array_test.rb index efa734f659..d0fa06c962 100644 --- a/test/stdlib/Array_test.rb +++ b/test/stdlib/Array_test.rb @@ -3,1025 +3,1360 @@ class ArraySingletonTest < Test::Unit::TestCase include TestHelper - testing "singleton(::Array)" + testing 'singleton(Array)' def test_new - assert_send_type "() -> ::Array[untyped]", + # Technically array has its own `Array.new` defined, but we don't actually define that as a signatuer + assert_send_type '() -> Array[untyped]', Array, :new - assert_send_type "(Array[Integer]) -> ::Array[Integer]", - Array, :new, [1,2,3] - assert_send_type "(Integer) -> Array[untyped]", - Array, :new, 3 - assert_send_type "(ToInt) -> Array[untyped]", - Array, :new, ToInt.new(3) - assert_send_type "(ToInt, String) -> Array[String]", - Array, :new, ToInt.new(3), "" - assert_send_type "(ToInt) { (Integer) -> :foo } -> Array[:foo]", - Array, :new, ToInt.new(3) do :foo end + with_array 1r, 2r do |array| + assert_send_type '(array[Rational]) -> Array[Rational]', + Array, :new, array + end + + with_int 5 do |size| + assert_send_type '(int) -> Array[nil]', + Array, :new, size + assert_send_type '(int, Rational) -> Array[Rational]', + Array, :new, size, 1r + assert_send_type '(int) { (Integer) -> Rational } -> Array[Rational]', + Array, :new, size do it.to_r end + end end - def test_square_bracket - assert_send_type "() -> Array[untyped]", - Array, :[] - assert_send_type "(Integer, String) -> Array[Integer | String]", - Array, :[], 1, "2" + def test_op_aref + assert_send_type '() -> Array[untyped]', + Array, :[] + assert_send_type '(Rational, String) -> Array[Rational | String]', + Array, :[], 1r, '2' end def test_try_convert - assert_send_type "(Integer) -> nil", - Array, :try_convert, 3 - assert_send_type "(ToArray) -> Array[Integer]", - Array, :try_convert, ToArray.new(1,2,3) + with [1r, 2r], Class.new(Array).new([1r, 2r]) do |subclass| + assert_send_type '[A < Array[U], U] (A) -> A', + Array, :try_convert, subclass + end + + with_array 1r, 2r do |ary| + assert_send_type '[U] (array[U]) -> Array[U]', + Array, :try_convert, ary + end + + with_untyped.and [1r, 2r] do |untyped| + assert_send_type '[U] (untyped) -> Array[U]?', + Array, :try_convert, untyped + end end end class ArrayInstanceTest < Test::Unit::TestCase include TestHelper - testing "::Array[::Integer]" - - def test_and - assert_send_type "(Array[Integer]) -> Array[Integer]", - [1,2,3], :&, [2,3,4] - assert_send_type "(ToArray) -> Array[Integer]", - [1,2,3], :&, ToArray.new(:a, :b, :c) - end - - def test_mul - assert_send_type "(Integer) -> Array[Integer]", - [1], :*, 3 - assert_send_type "(ToInt) -> Array[Integer]", - [1], :*, ToInt.new(3) - assert_send_type "(String) -> String", - [1], :*, "," - assert_send_type "(ToStr) -> String", - [1], :*, ToStr.new(",") - end - - def test_plus - assert_send_type "(Array[Integer]) -> Array[Integer]", - [1,2,3], :+, [4,5,6] - assert_send_type "(Array[String]) -> Array[Integer | String]", - [1,2,3], :+, ["4", "5", "6"] - assert_send_type "(ToArray) -> Array[Integer | String]", - [1,2,3], :+, ToArray.new("a") - - refute_send_type "(Enum) -> Array[Integer | String]", - [1,2,3], :+, Enum.new("a") - end - - def test_minus - assert_send_type "(Array[Integer]) -> Array[Integer]", - [1,2,3], :-, [4,5,6] - assert_send_type "(ToArray) -> Array[Integer | String]", - [1,2,3], :-, ToArray.new("a") - - refute_send_type "(Enum) -> Array[Integer]", - [1,2,3], :-, Enum.new("a") - end - - def test_lshift - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :<<, 4 - end - - def test_aref - assert_send_type "(Integer) -> Integer", - [1,2,3], :[], 0 - assert_send_type "(Integer) -> nil", - [1,2,3], :[], 1000 - assert_send_type "(Float) -> Integer", - [1,2,3], :[], 0.1 - assert_send_type "(ToInt) -> Integer", - [1], :[], ToInt.new(0) - - assert_send_type "(Integer, ToInt) -> Array[Integer]", - [1,2,3], :[], 0, ToInt.new(2) - assert_send_type "(Integer, ToInt) -> nil", - [1,2,3], :[], 4, ToInt.new(2) - - assert_send_type "(Range[Integer]) -> Array[Integer]", - [1,2,3], :[], 0...1 - assert_send_type "(Range[Integer]) -> nil", - [1,2,3], :[], 5..8 - assert_send_type "(Range[Integer?]) -> Array[Integer]", - [1,2,3], :[], 0...nil - end - - def test_aupdate - assert_send_type "(Integer, Integer) -> Integer", - [1,2,3], :[]=, 0, 0 - - assert_send_type "(Integer, ToInt, Integer) -> Integer", - [1,2,3], :[]=, 0, ToInt.new(2), -1 - assert_send_type "(Integer, ToInt, Array[Integer]) -> Array[Integer]", - [1,2,3], :[]=, 0, ToInt.new(2), [-1] - assert_send_type "(Integer, ToInt, nil) -> nil", - [1,2,3], :[]=, 0, ToInt.new(2), nil - - assert_send_type "(Range[Integer], Integer) -> Integer", - [1,2,3], :[]=, 0..2, -1 - assert_send_type "(Range[Integer], Array[Integer]) -> Array[Integer]", - [1,2,3], :[]=, 0...2, [-1] - assert_send_type "(Range[Integer], nil) -> nil", - [1,2,3], :[]=, 0...2, nil - assert_send_type "(Range[Integer?], Integer) -> Integer", - [1,2,3], :[]=, 0..nil, -1 + testing 'Array[Rational]' + + class ArraySubclass < Array end - def test_all? - assert_send_type "() -> bool", - [1,2,3], :all? - assert_send_type "(singleton(Integer)) -> bool", - [1,2,3], :all?, Integer - assert_send_type "() { (Integer) -> bool } -> bool", - [1,2,3], :all? do true end + class RngGen < BlankSlate + def rand(max) = ::RBS::UnitTest::Convertibles::ToInt.new(::Random.new.rand(max)) + end + + class ComparableToZero < BlankSlate + def initialize(cmp) = @cmp = cmp + def <(x) = 0.equal?(x) ? @cmp < x : fail + def >(x) = 0.equal?(x) ? @cmp > x : fail + end + + class HashKey < BlankSlate + protected attr_reader :key + def initialize(key) = @key = key + def hash = @key.hash + def eql?(other) = HashKey === other && @key.eql?(other.key) + end + + def with_each(*eles) + yield Each.new(*eles) + end + + def test_op_and + with_array [1r, 1i] do |other| + assert_send_type '(array[untyped]) -> Array[Rational]', + [1r, 2r], :&, other + end + end + + def test_op_times + with_string do |sep| + assert_send_type '(string) -> String', + [1r, 2r], :*, sep + end + + with_int 3 do |n| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :*, n + end + end + + def test_op_plus + with_array 3r, 4r do |ary| + assert_send_type '(array[Rational]) -> Array[Rational]', + [1r, 2r], :+, ary + end + + with_array 'a', 'b' do |ary| + assert_send_type '(array[String]) -> Array[Rational | String]', + [1r, 2r], :+, ary + end + end + + def test_op_sub + with_array 1r, 3r do |ary| + assert_send_type '(array[Rational]) -> Array[Rational]', + [1r, 2r], :-, ary + end + + with_untyped do |untyped| + with_array untyped do |ary| + assert_send_type '(array[untyped]) -> Array[Rational]', + [1r, 2r], :-, ary + end + end + end + + def test_op_lsh + assert_send_type '(Rational) -> Array[Rational]', + [1r, 2r, 3r], :<<, 4r + + assert_send_type '(Rational) -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r, 3r]), :<<, 4r + end + + def test_op_cmp + with_untyped.and [2r], [1r, 3r], [1r, 2r], [1r, 0r] do |other| + assert_send_type '(untyped) -> Integer?', + [1r, 2r], :<=>, other + end + end + + def test_op_eq + with_untyped.and [1r] do |untyped| + assert_send_type '(untyped) -> bool', + [1r], :==, untyped + end + end + + def test_op_aref(method: :[]) + with_int 1 do |index| + assert_send_type '(int) -> Rational', + [1r, 2r], method, index + assert_send_type '(int) -> nil', + [1r], method, index + end + + with_int 2 do |start| + with_int 1 do |length| + assert_send_type '(int, int) -> Array[Rational]', + [1r, 2r, 3r], method, start, length + assert_send_type '(int, int) -> nil', + [1r], method, start, length + end + end + + with_range with_int(2).and_nil, with_int(1).and_nil do |range| + assert_send_type '(range[int?]) -> Array[Rational]?', + [1r, 2r, 3r], method, range + end + end + + def test_slice + test_op_aref(method: :slice) + end + + def test_op_aset + with_int 1 do |index| + assert_send_type '(int, Rational) -> Rational', + [1r, 2r], :[]=, index, 3r + end + + with_range with_int(2).and_nil, with_int(1).and_nil do |range| + with_array 4r do |ary| + assert_send_type '[T < _ToAry[Rational]] (range[int?], T) -> T', + [1r, 2r, 3r], :[]=, range, ary + end + + assert_send_type '(range[int?], Rational) -> Rational', + [1r, 2r, 3r], :[]=, range, 4r + end + + with_int 2 do |start| + with_int 1 do |length| + with_array 4r do |ary| + assert_send_type '[T < _ToAry[Rational]] (int, int, T) -> T', + [1r, 2r, 3r], :[]=, start, length, ary + end + + assert_send_type '(int, int, Rational) -> Rational', + [1r, 2r, 3r], :[]=, start, length, 4r + end + end end def test_any? - assert_send_type "() -> bool", - [1,2,3], :any? - assert_send_type "(singleton(Integer)) -> bool", - [1,2,3], :any?, Integer - assert_send_type "() { (Integer) -> bool } -> bool", - [1,2,3], :any? do true end + assert_send_type '() -> bool', + [], :any? + assert_send_type '() -> bool', + [1r, 2r, 3r], :any? + assert_send_type '(singleton(Rational)) -> bool', + [1r, 2r, 3r], :any?, Rational + assert_send_type '() { (Rational) -> boolish } -> bool', + [1r, 2r, 3r], :any? do :true end + end + + def test_all? + assert_send_type '() -> bool', + [], :all? + assert_send_type '() -> bool', + [1r, 2r, 3r], :all? + assert_send_type '(singleton(Rational)) -> bool', + [1r, 2r, 3r], :all?, Rational + assert_send_type '() { (Rational) -> boolish } -> bool', + [1r, 2r, 3r], :all? do :true end end def test_assoc - assert_send_type "(Integer) -> nil", - [1,2,3], :assoc, 0 + with_untyped.and 1r, :a, 'b' do |object| + next unless defined? object.== + + assert_send_type '(untyped) -> Array[untyped]?', + [{foo: 3}, [1r, 1i], [:a, :b, :c], ['b']], :assoc, object + end end def test_at - assert_send_type "(Integer) -> Integer", - [1,2,3], :at, 0 - assert_send_type "(ToInt) -> Integer", - [1,2,3], :at, ToInt.new(0) - assert_send_type "(ToInt) -> nil", - [1,2,3], :at, ToInt.new(-5) + with_int 1 do |index| + assert_send_type '(int) -> Rational', + [1r, 2r], :at, index + assert_send_type '(int) -> nil', + [1r], :at, index + end end def test_bsearch - assert_send_type "() -> Enumerator[Integer]", [0,1,2,3,4], - :bsearch + ary = [0r, 4r, 7r, 10r, 12r] - assert_send_type "() { (Integer) -> (true | false) } -> Integer", - [0,1,2,3,4], :bsearch do |x| x > 2 end - assert_send_type "() { (Integer) -> (true | false) } -> nil", - [0,1,2,3,4], :bsearch do |x| x > 8 end + # Bool-based approach + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> Rational', + ary, :bsearch do |ele| ele >= 2 ? true : nil end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> nil', + ary, :bsearch do |ele| ele >= 100 ? true : nil end - assert_send_type "() { (Integer) -> Integer } -> Integer", - [0,1,2,3,4], :bsearch do |x| 3 <=> x end - assert_send_type "() { (Integer) -> Integer } -> nil", - [0,1,2,3,4], :bsearch do |x| 8 <=> x end + # Numeric-based approach + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> Rational', + ary, :bsearch do |ele| ele <=> 7 end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> Rational', + ary, :bsearch do |ele| 7.0 - ele end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> nil', + ary, :bsearch do |ele| ele <=> 100 end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> nil', + ary, :bsearch do |ele| 100.0 - ele end + # Enumerator + assert_send_type '() -> Enumerator[Rational, Rational?]', + ary, :bsearch end def test_bsearch_index - assert_send_type "() { (Integer) -> (true | false) } -> Integer", - [0,1,2,3,4], :bsearch_index do |x| x > 2 end - assert_send_type "() { (Integer) -> (true | false) } -> nil", - [0,1,2,3,4], :bsearch_index do |x| x > 8 end + ary = [0r, 4r, 7r, 10r, 12r] + + # Bool-based approach + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> Integer', + ary, :bsearch_index do |ele| ele >= 2 ? true : nil end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> nil', + ary, :bsearch_index do |ele| ele >= 100 ? true : nil end + + # Numeric-based approach + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> Integer', + ary, :bsearch_index do |ele| ele <=> 7 end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> Integer', + ary, :bsearch_index do |ele| 7.0 - ele end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> nil', + ary, :bsearch_index do |ele| ele <=> 100 end + assert_send_type '() { (Rational) -> (Numeric | bool?) } -> nil', + ary, :bsearch_index do |ele| 100.0 - ele end - assert_send_type "() { (Integer) -> Integer } -> Integer", - [0,1,2,3,4], :bsearch_index do |x| 3 <=> x end - assert_send_type "() { (Integer) -> Integer } -> nil", - [0,1,2,3,4], :bsearch_index do |x| 8 <=> x end + # Enumerator + assert_send_type '() -> Enumerator[Rational, Integer?]', + ary, :bsearch_index end - def test_llear - assert_send_type "() -> Array[Integer]", - [1,2,3], :clear + def test_clear + assert_send_type '() -> Array[Rational]', + [1r, 2r], :clear end - def test_collect - assert_send_type "() { (Integer) -> String } -> Array[String]", - [1,2,3], :collect do |x| x.to_s end - assert_send_type "() -> Enumerator[Integer, Array[untyped]]", - [1,2,3], :collect + def test_collect(method: :collect) + assert_send_type '() { (Rational) -> Complex } -> Array[Complex]', + [1r, 2r, 3r], method do |ele| Complex(ele) end + assert_send_type '() -> Enumerator[Rational, Array[untyped]]', + [1r, 2r, 3r], method end - def test_collect! - assert_send_type "() { (Integer) -> Integer } -> Array[Integer]", - [1,2,3], :collect! do |x| x+1 end + def test_map + test_collect(method: :map) end - def test_combination - assert_send_type "(Integer) -> Enumerator[Array[Integer], Array[Integer]]", - [1,2,3], :combination, 3 - assert_send_type "(ToInt) -> Enumerator[Array[Integer], Array[Integer]]", - [1,2,3], :combination, ToInt.new(3) + def test_collect!(method: :collect!) + assert_send_type '() { (Rational) -> Rational } -> Array[Rational]', + [1r, 2r, 3r], method do |ele| ele + 1 end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [], method + end - assert_send_type "(Integer) { (Array[Integer]) -> void } -> Array[Integer]", - [1,2,3], :combination, 3 do end - assert_send_type "(ToInt) { (Array[Integer]) -> void } -> Array[Integer]", - [1,2,3], :combination, ToInt.new(3) do end + def test_map! + test_collect!(method: :map!) + end + + def test_combination + with_int 2 do |count| + assert_send_type '(int) { (Array[Rational]) -> void } -> Array[Rational]', + [1r, 2r, 3r], :combination, count do end + assert_send_type '(int) -> Enumerator[Array[Rational], Array[Rational]]', + [1r, 2r, 3r], :combination, count + end end def test_compact - assert_send_type "() -> Array[Integer]", - [1,2,3], :compact - assert_send_type "() -> nil", - [1,2,3], :compact! + assert_send_type '() -> Array[Rational]', + [1r, 2r, 3r], :compact + assert_send_type '() -> Array[Rational]', + [1r, 2r, nil, 3r], :compact + end + + def test_compact! + assert_send_type '() -> nil', + [1r, 2r, 3r], :compact! + assert_send_type '() -> Array[Rational]', + [1r, 2r, nil, 3r], :compact! end def test_concat - assert_send_type "(Array[Integer], Array[Integer]) -> Array[Integer]", - [1,2,3], :concat, [4,5,6], [7,8,9] - assert_send_type "(Array[Integer], Array[Integer]) -> Array[Integer]", - Class.new(Array).new, :concat, [4,5,6], [7,8,9] + assert_send_type '() -> Array[Rational]', + [1r, 2r], :concat + with_array 3r do |array1| + assert_send_type '(*array[Rational]) -> Array[Rational]', + [1r, 2r], :concat, array1 + + with_array 4r do |array2| + assert_send_type '(*array[Rational]) -> Array[Rational]', + [1r, 2r], :concat, array1, array2 + end + end end def test_count - assert_send_type "() -> Integer", - [1,2,3], :count - assert_send_type "(Integer) -> Integer", - [1,2,3], :count, 1 - assert_send_type "() { (Integer) -> bool } -> Integer", - [1,2,3], :count do |x| x.odd? end + assert_send_type '() -> Integer', + [], :count + assert_send_type '() -> Integer', + [1r], :count + assert_send_type '() { (Rational) -> boolish } -> Integer', + [1r, 2r, 3r], :count do |x| x >= 2 end + + with_untyped.and 1r do |untyped| + next unless defined? untyped.== + assert_send_type '(untyped) -> Integer', + [1r, 2r], :count, untyped + end end def test_cycle - assert_send_type( - "() { (Integer) -> void } -> nil", - [1,2,3], :cycle, &proc { break_from_block } - ) + assert_send_type '() { (Rational) -> void } -> nil', + [1r, 2r, 3r], :cycle do break_from_block end + + # Unfortunately you can't test the enumerator branch because it'll infinitely loop + raise unless [1r, 2r, 3r].cycle.instance_of? Enumerator - assert_send_type "(Integer) { (Integer) -> void } -> nil", - [1,2,3], :cycle, 3 do end - assert_send_type "(ToInt) { (Integer) -> void } -> nil", - [1,2,3], :cycle, ToInt.new(2) do end + with_int(2).and_nil do |count| + assert_send_type '(int?) { (Rational) -> void } -> nil', + [1r, 2r, 3r], :cycle, count do break_from_block end + + if nil === count + raise unless [1r, 2r, 3r].cycle(test_count).instance_of? Enumerator + next + end + + assert_send_type '(int) -> Enumerator[Rational, nil]', + [1r, 2r, 3r], :cycle, count + end end def test_deconstruct - assert_send_type "() -> Array[Integer]", - [1,2,3], :deconstruct + assert_send_type '() -> Array[Rational]', + [1r, 2r], :deconstruct + assert_send_type '() -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r]), :deconstruct end def test_delete - assert_send_type "(Integer) -> Integer", - [1,2,3], :delete, 2 + with_untyped.and 1r do |untyped| + next unless defined? untyped.== - assert_send_type "(Integer) { (Integer) -> String } -> Integer", - [1,2,3], :delete, 2 do "" end - assert_send_type "(Symbol) { (Symbol) -> String } -> String", - [1,2,3], :delete, :foo do "" end + assert_send_type '(untyped) -> Rational?', + [1r], :delete, untyped + assert_send_type '[S] (S) { (S) -> Complex } -> (Rational | Complex) ', + [1r], :delete, untyped do 1i end + end end def test_delete_at - assert_send_type "(Integer) -> Integer", - [1,2,3], :delete_at, 2 - assert_send_type "(Integer) -> nil", - [1,2,3], :delete_at, 100 - - assert_send_type "(ToInt) -> nil", - [1,2,3], :delete_at, ToInt.new(300) + with_int 2 do |index| + assert_send_type '(int) -> Rational', + [1r, 2r, 3r], :delete_at, index + assert_send_type '(int) -> nil', + [1r, 2r], :delete_at, index + end end - def test_delete_if - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :delete_if do |x| x.odd? end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :delete_if + def test_delete_if(method: :delete_if) + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], method do |ele| ele > 2 ? :truthy : nil end + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], method do false end + assert_send_type '() -> Enumerator[Rational, Array[Rational]?]', + [], method end def test_difference - assert_send_type "(Array[Integer]) -> Array[Integer]", - [1,2,3], :difference, [2] + assert_send_type '() -> Array[Rational]', + [1r, 2r], :difference + + with_array 2r, 3r do |array1| + with_array 1, :a, 'b' do |array2| + assert_send_type '(*array[untyped]) -> Array[Rational]', + [1r, 2r], :difference, array1, array2 + end + end end def test_dig - assert_send_type "(Integer) -> Integer", - [1,2,3], :dig, 1 - assert_send_type "(Integer) -> nil", - [1,2,3], :dig, 10 - assert_send_type "(ToInt) -> nil", - [1,2,3], :dig, ToInt.new(10) + with_int 2 do |index| + assert_send_type '(int) -> nil', + [1r, 2r], :dig, index + assert_send_type '(int) -> Rational', + [1r, 2r, 3r], :dig, index + + assert_send_type '(int, untyped) -> untyped', + [1r, [3]], :dig, index, 0 + assert_send_type '(int, untyped, *untyped) -> untyped', + [1r, [[3]]], :dig, index, 0, 0 + + assert_send_type '(int, untyped) -> untyped', + [1r, 2r, [3]], :dig, index, 0 + assert_send_type '(int, untyped, *untyped) -> untyped', + [1r, 2r, [[3]]], :dig, index, 0, 0 + end end def test_drop - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :drop, 2 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :drop, ToInt.new(2) + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :drop, count + end end def test_drop_while - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :drop_while do false end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :drop_while + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], :take_while do |ele| ele >= 2 ? :truthy : nil end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [1r, 2r, 3r], :take_while end def test_each - assert_send_type "() { (Integer) -> void } -> Array[Integer]", - [1,2,3], :each do end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :each + assert_send_type '() { (Rational) -> void } -> Array[Rational]', + [1r, 2r, 3r], :each do end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [1r, 2r, 3r], :each end def test_each_index - assert_send_type "() { (Integer) -> void } -> Array[String]", - ['1','2','3'], :each_index do end - assert_send_type "() -> Enumerator[Integer, Array[String]]", - ['1','2','3'], :each_index + assert_send_type '() { (Integer) -> void } -> Array[Rational]', + [1r, 2r, 3r], :each_index do end + assert_send_type '() -> Enumerator[Integer, Array[Rational]]', + [1r, 2r, 3r], :each_index end def test_empty? - assert_send_type "() -> bool", - [1,2,3], :empty? + assert_send_type '() -> bool', + [], :empty? + assert_send_type '() -> bool', + [1r], :empty? end - def test_fetch - assert_send_type "(Integer) -> Integer", - [1,2,3], :fetch, 1 - assert_send_type "(ToInt) -> Integer", - [1,2,3], :fetch, ToInt.new(1) - - assert_send_type "(ToInt, String) -> Integer", - [1,2,3], :fetch, ToInt.new(1), "foo" - assert_send_type "(ToInt, String) -> String", - [1,2,3], :fetch, ToInt.new(10), "foo" + def test_eql? + with_untyped.and [1r] do |untyped| + assert_send_type '(untyped) -> bool', + [1r], :eql?, untyped + end + end - assert_send_type "(Integer) { (Integer) -> Symbol } -> Integer", - [1,2,3], :fetch, 1 do :hello end - assert_send_type "(Integer) { (Integer) -> Symbol } -> Symbol", - [1,2,3], :fetch, 10 do :hello end + def test_fetch + with_int 2 do |index| + assert_send_type '(int) -> Rational', + [1r, 2r, 3r], :fetch, index + + assert_send_type '(int, Complex) -> Complex', + [1r, 2r], :fetch, index, 1i + assert_send_type '(int, Complex) -> Rational', + [1r, 2r, 3r], :fetch, index, 1i + + assert_send_type '[I < _ToInt] (I) { (I) -> Complex } -> Complex', + [1r, 2r], :fetch, index do 1i end + assert_send_type '[I < _ToInt] (I) { (I) -> Complex } -> Rational', + [1r, 2r, 3r], :fetch, index do 1i end + end end def test_fetch_values - if_ruby("3.4"...) do - with_int(1) do |one| - with_int(2) do |two| - assert_send_type( - "(int, int) -> Array[Symbol]", - [:a, :b, :c], :fetch_values, one, two - ) - end + assert_send_type '() -> Array[Rational]', + [1r, 2r], :fetch_values + assert_send_type '() { (Integer) -> void } -> Array[Rational]', + [1r, 2r], :fetch_values do end + + with_int 1 do |index1| + with_int 2 do |index2| + assert_send_type '(*int) -> Array[Rational]', + [1r, 2r, 3r], :fetch_values, index1, index2 + assert_send_type '[I < _ToInt] (*I) { (I) -> void } -> Array[Rational]', + [1r, 2r, 3r], :fetch_values, index1, index2 do fail end + assert_send_type '[I < _ToInt] (*I) { (I) -> Complex } -> Array[Rational | Complex]', + [1r, 2r], :fetch_values, index1, index2 do |x| x.to_int.i end end - assert_send_type( - "() -> Array[Symbol]", - [:a, :b, :c], :fetch_values - ) end end def test_fill - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :fill, 0 - - assert_send_type "(Integer, nil) -> Array[Integer]", - [1,2,3], :fill, 0, nil - assert_send_type "(Integer, ToInt) -> Array[Integer]", - [1,2,3], :fill, 0, ToInt.new(1) - assert_send_type "(Integer, Integer) -> Array[Integer]", - [1,2,3], :fill, 0, 1 - assert_send_type "(Integer, Integer, Integer) -> Array[Integer]", - [1,2,3], :fill, 0, 1, 2 - assert_send_type "(Integer, ToInt, ToInt) -> Array[Integer]", - [1,2,3], :fill, 0, ToInt.new(1), ToInt.new(2) - assert_send_type "(Integer, Integer, nil) -> Array[Integer]", - [1,2,3], :fill, 0, 1, nil - - assert_send_type "(Integer, Range[Integer]) -> Array[Integer]", - [1,2,3], :fill, 0, 1..2 - - assert_send_type "() { (Integer) -> Integer } -> Array[Integer]", - [1,2,3], :fill do |i| i * 10 end - assert_send_type "(ToInt, ToInt) { (Integer) -> Integer } -> Array[Integer]", - [1,2,3], :fill, ToInt.new(0), ToInt.new(2) do |i| i * 10 end - assert_send_type "(Range[Integer]) { (Integer) -> Integer } -> Array[Integer]", - [1,2,3], :fill, 0..2 do |i| i * 10 end - end + assert_send_type '(Rational) -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill, 2r + with_int(1).and_nil do |start| + assert_send_type '(Rational, int?) -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill, 2r, start + + with_int(3).and_nil do |length| + assert_send_type '(Rational, int?, int?) -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill, 2r, start, length + end + end - def test_filter - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :filter do |i| i < 1 end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :filter - end + with_range with_int(1).and_nil, with_int(3).and_nil do |range| + assert_send_type '(Rational, range[int?]) -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill, 2r, range + end - def test_filter! - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :filter! do |i| i < 0 end - assert_send_type "() { (Integer) -> bool } -> nil", - [1,2,3], :filter! do |i| i > 0 end + assert_send_type '() { (Integer) -> Rational } -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill do 1r end + + with_int(1).and_nil do |start| + assert_send_type '(int?) { (Integer) -> Rational } -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill, start do 1r end - assert_send_type "() -> Enumerator[Integer, Array[Integer]?]", - [1,2,3], :filter! + with_int(3).and_nil do |length| + assert_send_type '(int?, int?) { (Integer) -> Rational } -> Array[Rational]', + [1r, 2r, 3r, 4r], :fill, start, length do 1r end + end + end end - def test_find - assert_send_type "() { (Integer) -> bool } -> Integer", - [1,2,3], :find, &->(i) { i.odd? } - assert_send_type "() { (Integer) -> bool } -> nil", - [0,2], :find, &->(i) { i.odd? } - assert_send_type "(Enumerable::_NotFound[String]) { (Integer) -> bool } -> String", - [0,2], :find, -> { "" }, &->(i) { i.odd? } + def test_find(method: :find) + assert_send_type '() { (Rational) -> boolish } -> Rational', + [1r, 2r, 3r], method do it > 2 ? :truthy : nil end + assert_send_type '() { (Rational) -> boolish } -> nil', + [1r, 2r, 3r], method do false end + assert_send_type '() -> Enumerator[Rational, Rational?]', + [1r, 2r, 3r], method - assert_send_type "() -> Enumerator[Integer, Integer?]", - [1,2,3], :find - assert_send_type "(Enumerable::_NotFound[String]) -> Enumerator[Integer, Integer | String | nil]", - [0,2], :find, -> { "" } + assert_send_type '(Enumerable::_NotFound[String]) { (Rational) -> boolish } -> Rational', + [1r, 2r, 3r], method, ->{ "" } do it > 2 ? :truthy : nil end + assert_send_type '(Enumerable::_NotFound[String]) { (Rational) -> boolish } -> String', + [1r, 2r, 3r], method, ->{ "" } do false end + assert_send_type '(Enumerable::_NotFound[String]) -> Enumerator[Rational, Rational | String]', + [1r, 2r, 3r], method, ->{ "" } end - def test_find_index - assert_send_type "(Integer) -> Integer", - [1,2,3], :find_index, 1 - assert_send_type "(String) -> nil", - [1,2,3], :find_index, "0" + def test_detect + test_find(method: :detect) + end + + def test_find_index(method: :find_index) + with_untyped.and 2r do |untyped| + next unless defined? untyped.== + assert_send_type '(untyped) -> Integer?', + [1r, 2r, 3r], method, untyped + end - assert_send_type "() { (Integer) -> bool } -> Integer", - [1,2,3], :find_index do |i| i.odd? end - assert_send_type "() { (Integer) -> bool } -> nil", - [1,2,3], :find_index do |i| i < 0 end + assert_send_type '() { (Rational) -> boolish } -> Integer', + [1r, 2r, 3r], method do :true end + assert_send_type '() { (Rational) -> boolish } -> nil', + [1r, 2r, 3r], method do false end - assert_send_type "() -> Enumerator[Integer, Integer?]", - [1,2,3], :find_index + assert_send_type '() -> Enumerator[Rational, Integer?]', + [1r, 2r, 3r], method end - def test_first - assert_send_type "() -> Integer", - [1,2,3], :first - assert_send_type "() -> nil", - [], :first + def test_index + test_find_index(method: :index) + end - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :first, 2 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :first, ToInt.new(2) - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :first, 0 + def test_first + assert_send_type '() -> nil', + [], :first + assert_send_type '() -> Rational', + [1r, 2r], :first + + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [], :first, count + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :first, count + end end def test_flatten - assert_send_type "() -> Array[untyped]", - [1,2,3], :flatten - assert_send_type "(Integer) -> Array[untyped]", - [1,2,3], :flatten, 3 - assert_send_type "(ToInt) -> Array[untyped]", - [1,2,3], :flatten, ToInt.new(3) + assert_send_type '() -> Array[untyped]', + [[1r, 2r]], :flatten + + with_int(1).and_nil do |level| + assert_send_type '(int?) -> Array[untyped]', + [[1r, 2r]], :flatten, level + end end def test_flatten! - assert_send_type "() -> Array[untyped]?", - [1,2,3], :flatten! - assert_send_type "(Integer) -> Array[untyped]?", - [1,2,3], :flatten!, 3 - assert_send_type "(ToInt) -> Array[untyped]?", - [1,2,3], :flatten!, ToInt.new(3) + assert_send_type '() -> nil', + ArraySubclass.new([1r, 2r]), :flatten! + assert_send_type '() -> ArrayInstanceTest::ArraySubclass[untyped]', + ArraySubclass.new([[1r, 2r]]), :flatten! + + with_int(1).and_nil do |level| + assert_send_type '(int?) -> nil', + ArraySubclass.new([1r, 2r]), :flatten!, level + assert_send_type '(int?) -> ArrayInstanceTest::ArraySubclass[untyped]', + ArraySubclass.new([[1r, 2r]]), :flatten!, level + end end - def test_include? - assert_send_type "(Integer) -> bool", - [1,2,3], :include?, 1 + def test_hash + assert_send_type '() -> Integer', + [], :hash + assert_send_type '() -> Integer', + [1r, 2r], :hash end - def test_index - assert_send_type "(Integer) -> Integer", - [1,2,3], :index, 1 - assert_send_type "(String) -> nil", - [1,2,3], :index, "0" - - assert_send_type "() { (Integer) -> bool } -> Integer", - [1,2,3], :index do |i| i.odd? end - assert_send_type "() { (Integer) -> bool } -> nil", - [1,2,3], :index do |i| i < 0 end - - assert_send_type "() -> Enumerator[Integer, Integer?]", - [1,2,3], :index + def test_include? + with_untyped.and 1r do |untyped| + next unless defined? untyped.== + assert_send_type '(top) -> bool', + [1r, 2r], :include?, untyped + end end def test_insert - assert_send_type "(Integer, Integer) -> Array[Integer]", - [1,2,3], :insert, 0, 10 - assert_send_type "(ToInt, Integer, Integer, Integer) -> Array[Integer]", - [1,2,3], :insert, ToInt.new(0), 10, 20, 30 + with_int 1 do |index| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r, 3r], :insert, index + assert_send_type '(int, *Rational) -> Array[Rational]', + [1r, 2r, 3r], :insert, index, 3r, 4r + end + end - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :insert, 0 + def test_inspect(method: :inspect) + assert_send_type '() -> String', + [], method + assert_send_type '() -> String', + [1r, 2r], method + end + + def test_to_s + test_inspect(method: :to_s) end def test_intersect? - assert_send_type( - "(Array[Integer]) -> bool", - [1,2,3], :intersect?, [0] - ) - assert_send_type( - "(ToArray[Integer]) -> bool", - [1,2,3], :intersect?, ToArray.new([0, 1]) - ) + with_array 1r, 3r do |array| + assert_send_type '(array[untyped]) -> bool', + [1r, 2r, 3r], :intersect?, array + end + + with_array :a, :b do |array| + assert_send_type '(array[untyped]) -> bool', + [1r, 2r, 3r], :intersect?, array + end end def test_intersection - assert_send_type "(Array[Integer]) -> Array[Integer]", - [1,2,3], :intersection, [2,3,4] - assert_send_type "(Array[Integer], Array[String], ToArray) -> Array[Integer]", - [1,2,3], :intersection, [2,3,4], ["a"], ToArray.new(true, false) - assert_send_type "() -> Array[Integer]", - [1,2,3], :intersection + assert_send_type '() -> Array[Rational]', + [1r, 2r, 3r], :intersection + + with_array 1r, 2r, 4r do |array1| + with_array 1, 2, 4 do |array2| + assert_send_type '(*array[untyped]) -> Array[Rational]', + [1r, 2r, 3r], :intersection, array1, array2 + end + end end def test_join - assert_send_type "() -> String", - [1,2,3], :join + assert_send_type '() -> String', + [1r, 2r], :join - assert_send_type "(String) -> String", - [1,2,3], :join, "," - assert_send_type "(ToStr) -> String", - [1,2,3], :join, ToStr.new(",") + with_string("x").and_nil do |sep| + assert_send_type '(string?) -> String', + [1r, 2r], :join, sep + end end def test_keep_if - assert_send_type "() { (Integer) -> false } -> Array[Integer]", - [1,2,3], :keep_if do false end - assert_send_type "() { (Integer) -> true } -> Array[Integer]", - [1,2,3], :keep_if do true end - - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :keep_if + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], :keep_if do |ele| ele > 2 ? :truthy : nil end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [1r, 2r, 3r], :keep_if end def test_last - assert_send_type "() -> Integer", - [1,2,3], :last - assert_send_type "() -> nil", - [], :last - - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :last, 2 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :last, ToInt.new(0) - end - - def test_length - assert_send_type "() -> Integer", - [1,2,3], :length + assert_send_type '() -> nil', + [], :last + assert_send_type '() -> Rational', + [1r, 2r], :last + + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [], :last, count + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :last, count + end end - def test_map - assert_send_type "() { (Integer) -> String } -> Array[String]", - [1,2,3], :map do |x| x.to_s end - assert_send_type "() -> Enumerator[Integer, Array[untyped]]", - [1,2,3], :map + def test_length(method: :length) + assert_send_type '() -> Integer', + [], method + assert_send_type '() -> Integer', + [1r, 2r], method end - def test_map! - assert_send_type "() { (Integer) -> Integer } -> Array[Integer]", - [1,2,3], :map! do |x| x+1 end + def test_size + test_length(method: :size) end def test_max - assert_send_type "() -> Integer", - [1,2,3], :max - assert_send_type "() -> nil", - [], :max + ary = 10.times.map { |x| Rational(x) }.shuffle + + assert_send_type '() -> nil', + [], :max + assert_send_type '() -> Rational', + ary, :max + assert_send_type '() { (Rational, Rational) -> Comparable::_CompareToZero } -> Rational', + ary, :max do |a, b| ComparableToZero.new(a <=> b) end + + with_int 3 do |count| + assert_send_type '(int) -> Array[Rational]', + ary, :max, count + assert_send_type '(int) { (Rational, Rational) -> Comparable::_CompareToZero } -> Array[Rational]', + ary, :max, count do |a, b| ComparableToZero.new(a <=> b) end + end + end - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :max, 1 - assert_send_type "(ToInt) -> Array[Integer]", - [], :max, ToInt.new(1) + def test_min + ary = 10.times.map { |x| Rational(x) }.shuffle - assert_send_type "() { (Integer, Integer) -> Integer } -> Integer", - [1,2,3], :max do |_, _| 1 end - assert_send_type "() { (Integer, Integer) -> Integer } -> nil", - [], :max do |_, _| 0 end + assert_send_type '() -> nil', + [], :min + assert_send_type '() -> Rational', + ary, :min + assert_send_type '() { (Rational, Rational) -> Comparable::_CompareToZero } -> Rational', + ary, :min do |a, b| ComparableToZero.new(a <=> b) end - assert_send_type "(ToInt) { (Integer, Integer) -> Integer } -> Array[Integer]", - [1,2,3], :max, ToInt.new(2) do |_, _| 0 end - refute_send_type "(Integer) { (Integer, Integer) -> ToInt } -> Array[Integer]", - [1,2,3], :max, 2 do |_, _| ToInt.new(0) end + with_int 3 do |count| + assert_send_type '(int) -> Array[Rational]', + ary, :min, count + assert_send_type '(int) { (Rational, Rational) -> Comparable::_CompareToZero } -> Array[Rational]', + ary, :min, count do |a, b| ComparableToZero.new(a <=> b) end + end end def test_minmax - assert_send_type "() -> [Integer, Integer]", - [1,2,3], :minmax - assert_send_type "() -> [Integer, Integer]", - [1], :minmax - assert_send_type "() -> [nil, nil]", - [], :minmax + ary = 10.times.map { |x| Rational(x) }.shuffle - assert_send_type "() { (Integer, Integer) -> Integer } -> [Integer, Integer]", - [1, 2], :minmax do |_, _| 0 end - end - - def test_none? - assert_send_type "() -> bool", - [1,2,3], :none? - assert_send_type "(singleton(String)) -> bool", - [1,2,3], :none?, String - assert_send_type "() { (Integer) -> bool } -> bool", - [1,2,3], :none? do |x| x.even? end - end - - def test_one? - assert_send_type "() -> bool", - [1,2,3], :one? - assert_send_type "(singleton(String)) -> bool", - [1,2,3], :one?, String - assert_send_type "() { (Integer) -> bool } -> bool", - [1,2,3], :one? do |x| x.even? end + assert_send_type '() -> [nil, nil]', + [], :minmax + assert_send_type '() { (Rational, Rational) -> Comparable::_CompareToZero } -> [nil, nil]', + [], :minmax do end + assert_send_type '() -> [Rational, Rational]', + ary, :minmax + assert_send_type '() { (Rational, Rational) -> Comparable::_CompareToZero } -> [Rational, Rational]', + ary, :minmax do |a, b| ComparableToZero.new(a <=> b) end end def test_pack - assert_send_type "(String) -> String", - [1,2,3], :pack, "ccc" - assert_send_type "(ToStr) -> String", - [1,2,3], :pack, ToStr.new("ccc") - - assert_send_type "(String, buffer: String) -> String", - [1,2,3], :pack, "ccc", buffer: +"" - assert_send_type "(String, buffer: nil) -> String", - [1,2,3], :pack, "ccc", buffer: nil - refute_send_type "(ToStr, buffer: ToStr) -> String", - [1,2,3], :pack, ToStr.new("ccc"), buffer: ToStr.new("") + with_string 'ccc' do |fmt| + assert_send_type '(string) -> String', + [1, 2, 3], :pack, fmt + + assert_send_type '(string, buffer: nil) -> String', + [1, 2, 3], :pack, fmt, buffer: nil + assert_send_type '(string, buffer: String) -> String', + [1, 2, 3], :pack, fmt, buffer: +'' + refute_send_type '(string, buffer: _ToStr) -> String', + [1,2,3], :pack, fmt, buffer: ToStr.new(+'') + end end def test_permutation - assert_send_type "(Integer) -> Enumerator[Array[Integer], Array[Integer]]", - [1,2,3], :permutation, 2 - assert_send_type "() -> Enumerator[Array[Integer], Array[Integer]]", - [1,2,3], :permutation - + assert_send_type '() { (Array[Rational]) -> void } -> Array[Rational]', + [1r, 2r, 3r], :permutation do end + assert_send_type '() -> Enumerator[Array[Rational], Array[Rational]]', + [1r, 2r, 3r], :permutation + + with_int(2).and_nil do |count| + assert_send_type '(int?) { (Array[Rational]) -> void } -> Array[Rational]', + [1r, 2r, 3r], :permutation, count do end + end - assert_send_type "(Integer) { (Array[Integer]) -> void } -> Array[Integer]", - [1,2,3], :permutation, 2 do end - assert_send_type "() { (Array[Integer]) -> void } -> Array[Integer]", - [1,2,3], :permutation do end + # Unfortunately ruby 4.0 and before had a bug where `Array#permutation` with a `nil` + # argument didn't have a correct size. cf https://github.com/ruby/ruby/pull/17197. + with_int 2 do |count| + assert_send_type '(int) -> Enumerator[Array[Rational], Array[Rational]]', + [1r, 2r, 3r], :permutation, count + end + if_ruby '4.2'... do + assert_send_type '(nil) -> Enumerator[Array[Rational], Array[Rational]]', + [1r, 2r, 3r], :permutation, nil + end end def test_pop - assert_send_type "() -> Integer", - [1,2,3], :pop - assert_send_type "() -> nil", - [], :pop - - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :pop, 1 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :pop, ToInt.new(2) + assert_send_type '() -> nil', + [], :pop + assert_send_type '() -> Rational', + [1r], :pop + + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r, 3r], :pop, count + end end def test_product - assert_send_type "() -> Array[[Integer]]", - [1,2,3], :product - assert_send_type "(Array[String]) -> Array[[Integer, String]]", - [1,2,3], :product, ["a", "b"] - assert_send_type "(Array[String], Array[Symbol]) -> Array[[Integer, String, Symbol]]", - [1,2,3], :product, ["a", "b"], [:a, :b] - assert_send_type "(Array[String], Array[Symbol], Array[true | false]) -> Array[Array[Integer | interned | true | false]]", - [1,2,3], :product, ["a", "b"], [:a, :b], [true, false] - end - - def test_push - assert_send_type "() -> Array[Integer]", - [1,2,3], :push - assert_send_type "(Integer, Integer) -> Array[Integer]", - [1,2,3], :push, 4, 5 + assert_send_type '() -> Array[[Rational]]', + [1r, 2r], :product + assert_send_type '() { ([Rational]) -> void } -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r]), :product do end + + with_array 1i, 2i do |array1| + assert_send_type '(array[Complex]) -> Array[[Rational, Complex]]', + [1r, 2r], :product, array1 + assert_send_type '(array[Complex]) { ([Rational, Complex]) -> void } -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r]), :product, array1 do end + + with_array 1.0, 2.0 do |array2| + assert_send_type '(array[Complex], array[Float]) -> Array[[Rational, Complex, Float]]', + [1r, 2r], :product, array1, array2 + assert_send_type '(array[Complex], array[Float]) { ([Rational, Complex, Float]) -> void } -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r]), :product, array1, array2 do end + + with_array 1, 2 do |array3| + assert_send_type '(*array[Complex | Float | Integer]) -> Array[Array[Rational | Complex | Float | Integer]]', + [1r, 2r], :product, array1, array2, array3 + assert_send_type '(*array[Complex | Float | Integer]) { (Array[Rational | Complex | Float | Integer]) -> void } -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r]), :product, array1, array2, array3 do end + end + end + end end - def test_rassoc - assert_send_type "(String) -> nil", - [1,2,3], :rassoc, "3" + def test_push(method: :push) + assert_send_type '() -> Array[Rational]', + [1r], method + assert_send_type '(*Rational) -> Array[Rational]', + [1r], method, 2r + assert_send_type '(*Rational) -> Array[Rational]', + [1r], method, 2r, 3r end - def test_reject - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :reject do |x| x.odd? end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :reject + def test_append + test_push(method: :append) end def test_reject! - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :reject! do |x| x.odd? end - assert_send_type "() { (Integer) -> bool } -> nil", - [1,2,3], :reject! do |x| x == "" end - - assert_send_type "() -> Enumerator[Integer, Array[Integer]?]", - [1,2,3], :reject! + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], :reject! do |ele| ele > 2 ? :truthy : nil end + assert_send_type '() { (Rational) -> boolish } -> nil', + [1r, 2r, 3r], :reject! do false end + assert_send_type '() -> Enumerator[Rational, Array[Rational]?]', + [], :reject! end def test_repeated_combination - assert_send_type "(ToInt) { (Array[Integer]) -> nil } -> Array[Integer]", - [1,2,3], :repeated_combination, ToInt.new(2) do end - assert_send_type "(ToInt) -> Enumerator[Array[Integer], Array[Integer]]", - [1,2,3], :repeated_combination, ToInt.new(2) + with_int 2 do |count| + assert_send_type '(int) { (Array[Rational]) -> void } -> Array[Rational]', + [1r, 2r, 3r], :repeated_combination, count do end + assert_send_type '(int) -> Enumerator[Array[Rational], Array[Rational]]', + [1r, 2r, 3r], :repeated_combination, count + end end def test_repeated_permutation - assert_send_type "(ToInt) { (Array[Integer]) -> nil } -> Array[Integer]", - [1,2,3], :repeated_permutation, ToInt.new(2) do end - assert_send_type "(ToInt) -> Enumerator[Array[Integer], Array[Integer]]", - [1,2,3], :repeated_permutation, ToInt.new(2) + with_int 2 do |count| + assert_send_type '(int) { (Array[Rational]) -> void } -> Array[Rational]', + [1r, 2r, 3r], :repeated_permutation, count do end + assert_send_type '(int) -> Enumerator[Array[Rational], Array[Rational]]', + [1r, 2r, 3r], :repeated_permutation, count + end end - def test_replace - assert_send_type "(Array[Integer]) -> Array[Integer]", - [1,2,3], :replace, [2,3,4] + def test_replace(method: :replace) + with_array 3r, 4r do |array| + assert_send_type '(array[Rational]) -> Array[Rational]', + [1r, 2r], method, array + end end def test_reverse - assert_send_type "() -> Array[Integer]", - [1,2,3], :reverse + assert_send_type '() -> Array[Rational]', + [], :reverse + assert_send_type '() -> Array[Rational]', + [1r], :reverse + assert_send_type '() -> Array[Rational]', + [1r, 2r], :reverse end def test_reverse! - assert_send_type "() -> Array[Integer]", - [1,2,3], :reverse! + assert_send_type '() -> Array[Rational]', + [], :reverse! + assert_send_type '() -> Array[Rational]', + [1r], :reverse! + assert_send_type '() -> Array[Rational]', + [1r, 2r], :reverse! end def test_reverse_each - assert_send_type "() { (Integer) -> nil } -> Array[Integer]", - [1,2,3], :reverse_each do end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [2,3,4], :reverse_each + assert_send_type '() { (Rational) -> void } -> Array[Rational]', + [1r, 2r, 3r], :reverse_each do end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [1r, 2r, 3r], :reverse_each end def test_rfind - assert_send_type "() { (Integer) -> bool } -> Integer", - [1,2,3], :rfind, &->(i) { i.odd? } - assert_send_type "() { (Integer) -> bool } -> nil", - [0,2], :rfind, &->(i) { i.odd? } - assert_send_type "(Enumerable::_NotFound[String]) { (Integer) -> bool } -> String", - [0,2], :rfind, -> { "" }, &->(i) { i.odd? } + assert_send_type '() { (Rational) -> boolish } -> Rational', + [1r, 2r, 3r], :rfind do it > 2 ? :truthy : nil end + assert_send_type '() { (Rational) -> boolish } -> nil', + [1r, 2r, 3r], :rfind do false end + assert_send_type '() -> Enumerator[Rational, Rational?]', + [1r, 2r, 3r], :rfind - assert_send_type "() -> Enumerator[Integer, Integer?]", - [1,2,3], :rfind - assert_send_type "(Enumerable::_NotFound[String]) -> Enumerator[Integer, Integer | String | nil]", - [0,2], :rfind, -> { "" } + assert_send_type '(Enumerable::_NotFound[String]) { (Rational) -> boolish } -> Rational', + [1r, 2r, 3r], :rfind, ->{ "" } do it > 2 ? :truthy : nil end + assert_send_type '(Enumerable::_NotFound[String]) { (Rational) -> boolish } -> String', + [1r, 2r, 3r], :rfind, ->{ "" } do false end + assert_send_type '(Enumerable::_NotFound[String]) -> Enumerator[Rational, Rational | String]', + [1r, 2r, 3r], :rfind, ->{ "" } end def test_rindex - assert_send_type "(Integer) -> Integer", - [1,2,3], :rindex, 1 - assert_send_type "(String) -> nil", - [1,2,3], :rindex, "0" + with_untyped.and 2r do |untyped| + next unless defined? untyped.== + assert_send_type '(untyped) -> Integer?', + [1r, 2r, 3r], :rindex, untyped + end - assert_send_type "() { (Integer) -> bool } -> Integer", - [1,2,3], :rindex do |i| i.odd? end - assert_send_type "() { (Integer) -> bool } -> nil", - [1,2,3], :rindex do |i| i < 0 end + assert_send_type '() { (Rational) -> boolish } -> Integer', + [1r, 2r, 3r], :rindex do :true end + assert_send_type '() { (Rational) -> boolish } -> nil', + [1r, 2r, 3r], :rindex do false end - assert_send_type "() -> Enumerator[Integer, Integer?]", - [1,2,3], :rindex + assert_send_type '() -> Enumerator[Rational, Integer?]', + [1r, 2r, 3r], :rindex end def test_rotate - assert_send_type "() -> Array[Integer]", - [1,2,3], :rotate - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :rotate, 3 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :rotate, ToInt.new(2) + assert_send_type '() -> Array[Rational]', + [], :rotate + assert_send_type '() -> Array[Rational]', + [1r], :rotate + assert_send_type '() -> Array[Rational]', + [1r, 2r], :rotate + with_int 1 do |count| + assert_send_type '(int) -> Array[Rational]', + [], :rotate, count + assert_send_type '(int) -> Array[Rational]', + [1r], :rotate, count + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :rotate, count + end end def test_rotate! - assert_send_type "() -> Array[Integer]", - [1,2,3], :rotate! - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :rotate!, 3 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :rotate!, ToInt.new(2) + assert_send_type '() -> Array[Rational]', + [], :rotate! + assert_send_type '() -> Array[Rational]', + [1r], :rotate! + assert_send_type '() -> Array[Rational]', + [1r, 2r], :rotate! + with_int 1 do |count| + assert_send_type '(int) -> Array[Rational]', + [], :rotate!, count + assert_send_type '(int) -> Array[Rational]', + [1r], :rotate!, count + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :rotate!, count + end end def test_sample - assert_send_type "() -> Integer", - [1,2,3], :sample - assert_send_type "() -> nil", - [], :sample - assert_send_type "(random: Random) -> Integer", - [1,2,3], :sample, random: Random.new(1) - assert_send_type "(random: Rand) -> Integer", - [1,2,3], :sample, random: Rand.new - - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :sample, 2 - assert_send_type "(ToInt, random: Random) -> Array[Integer]", - [1,2,3], :sample, ToInt.new(2), random: Random.new(2) - assert_send_type "(ToInt, random: Rand) -> Array[Integer]", - [1,2,3], :sample, ToInt.new(2), random: Rand.new - end - - def test_select - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :select do |i| i < 1 end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :select - end - - def test_select! - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :select! do |i| i < 1 end - assert_send_type "() { (Integer) -> bool } -> nil", - [1,2,3], :select! do true end - assert_send_type "() -> Enumerator[Integer, Array[Integer]?]", - [1,2,3], :select! + assert_send_type '() -> nil', + [], :sample + assert_send_type '(random: Array::_Rand) -> nil', + [], :sample, random: RngGen.new + + assert_send_type '() -> Rational', + [1r, 2r, 3r], :sample + assert_send_type '(random: Array::_Rand) -> Rational', + [1r, 2r, 3r], :sample, random: RngGen.new + + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r, 3r], :sample, count + assert_send_type '(int, random: Array::_Rand) -> Array[Rational]', + [1r, 2r, 3r], :sample, count, random: RngGen.new + end end - def test_shift - assert_send_type "() -> Integer", - [1,2,3], :shift - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :shift, ToInt.new(1) - assert_send_type "() -> nil", - [], :shift + def test_select(method: :select) + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], method do |ele| ele > 2 ? :truthy : nil end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [1r, 2r, 3r], method end - def test_shuffle - assert_send_type "() -> Array[Integer]", - [1,2,3], :shuffle - assert_send_type "(random: Random) -> Array[Integer]", - [1,2,3], :shuffle, random: Random.new(2) - assert_send_type "(random: Rand) -> Array[Integer]", - [1,2,3], :shuffle, random: Rand.new + def test_filter + test_select(method: :filter) end - def test_shuffle! - assert_send_type "() -> Array[Integer]", - [1,2,3], :shuffle! - assert_send_type "(random: Random) -> Array[Integer]", - [1,2,3], :shuffle!, random: Random.new(2) - assert_send_type "(random: Rand) -> Array[Integer]", - [1,2,3], :shuffle!, random: Rand.new + def test_select!(method: :select!) + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], method do |ele| ele > 2 ? :truthy : nil end + assert_send_type '() { (Rational) -> boolish } -> nil', + [1r, 2r, 3r], method do true end + assert_send_type '() -> Enumerator[Rational, Array[Rational]?]', + [], method end - def test_slice - assert_send_type "(Integer) -> Integer", - [1,2,3], :slice, 1 - assert_send_type "(ToInt) -> nil", - [1,2,3], :slice, ToInt.new(11) + def test_filter! + test_select(method: :filter!) + end + + def test_shift + assert_send_type '() -> nil', + [], :shift + assert_send_type '() -> Rational', + [1r], :shift + + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r, 3r], :shift, count + end + end - assert_send_type "(Integer, Integer) -> Array[Integer]", - [1,2,3], :slice, 1, 2 - assert_send_type "(ToInt, ToInt) -> nil", - [1,2,3], :slice, ToInt.new(10), ToInt.new(2) + def test_shuffle + assert_send_type '() -> Array[Rational]', + [1r, 2r, 3r], :shuffle + + assert_send_type '(random: Array::_Rand) -> Array[Rational]', + [1r, 2r, 3r], :shuffle, random: RngGen.new + end + + def test_shuffle! + assert_send_type '() -> Array[Rational]', + [1r, 2r, 3r], :shuffle! - assert_send_type "(Range[Integer]) -> Array[Integer]", - [1,2,3], :slice, 1...2 - assert_send_type "(Range[Integer]) -> nil", - [1,2,3], :slice, 11...21 + assert_send_type '(random: Array::_Rand) -> Array[Rational]', + [1r, 2r, 3r], :shuffle!, random: RngGen.new end def test_slice! - assert_send_type "(Integer) -> Integer", - [1,2,3], :slice!, 1 - assert_send_type "(ToInt) -> nil", - [1,2,3], :slice!, ToInt.new(11) + with_int 1 do |index| + assert_send_type '(int) -> Rational', + [1r, 2r], :slice!, index + assert_send_type '(int) -> nil', + [1r], :slice!, index + end - assert_send_type "(Integer, Integer) -> Array[Integer]", - [1,2,3], :slice!, 1, 2 - assert_send_type "(ToInt, ToInt) -> nil", - [1,2,3], :slice!, ToInt.new(10), ToInt.new(2) + with_int 2 do |start| + with_int 1 do |length| + assert_send_type '(int, int) -> Array[Rational]', + [1r, 2r, 3r], :slice!, start, length + assert_send_type '(int, int) -> nil', + [1r], :slice!, start, length + end + end - assert_send_type "(Range[Integer]) -> Array[Integer]", - [1,2,3], :slice!, 1...2 - assert_send_type "(Range[Integer]) -> nil", - [1,2,3], :slice!, 11...21 + with_range with_int(2).and_nil, with_int(1).and_nil do |range| + assert_send_type '(range[int?]) -> Array[Rational]?', + [1r, 2r, 3r], :slice!, range + end end def test_sort - assert_send_type "() -> Array[Integer]", - [1,2,3], :sort - - assert_send_type "() { (Integer, Integer) -> Integer } -> Array[Integer]", - [1,2,3], :sort do |a, b| b <=> a end + ary = 10.times.map { |x| Rational(x) }.shuffle - refute_send_type "() { (Integer, Integer) -> nil } -> Array[Integer]", - [1,2,3], :sort do end + assert_send_type '() -> Array[Rational]', + ary, :sort + assert_send_type '() { (Rational, Rational) -> Comparable::_CompareToZero } -> Array[Rational]', + ary, :sort do |a, b| ComparableToZero.new(a <=> b) end end def test_sort! - assert_send_type "() -> Array[Integer]", - [1,2,3], :sort! + ary = 10.times.map { |x| Rational(x) }.shuffle - assert_send_type "() { (Integer, Integer) -> Integer } -> Array[Integer]", - [1,2,3], :sort! do |a, b| b <=> a end - - refute_send_type "() { (Integer, Integer) -> nil } -> Array[Integer]", - [1,2,3], :sort! do end + assert_send_type '() -> Array[Rational]', + ary, :sort! + assert_send_type '() { (Rational, Rational) -> Comparable::_CompareToZero } -> Array[Rational]', + ary, :sort! do |a, b| ComparableToZero.new(a <=> b) end end def test_sort_by! - assert_send_type "() { (Integer) -> String } -> Array[Integer]", - [1,2,3], :sort_by! do |x| x.to_s end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :sort_by! + ary = 10.times.map { |x| Rational(x) }.shuffle + + assert_send_type '() { (Rational) -> Comparable::_WithSpaceshipOperator } -> Array[Rational]', + ary, :sort_by! do |x| + bs = BlankSlate.new + ::Kernel.instance_method(:instance_variable_set).bind_call(bs, :@x, x) + def bs.x = @x + def bs.<=>(r) + cmp = BlankSlate.new + ::Kernel.instance_method(:instance_variable_set) + .bind_call(cmp, :@cmp, @x <=> r.x) + def cmp.<(x) = 0.equal?(x) ? @cmp < 0 : fail + def cmp.>(x) = 0.equal?(x) ? @cmp > 0 : fail + cmp + end + bs + end + + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + ary, :sort_by! + end def test_sum - assert_send_type "() -> Integer", - [1,2,3], :sum - assert_send_type "(Integer) -> Integer", - [1,2,3], :sum, 2 + assert_send_type '() -> untyped', + [1, 2r, 3.0, 4i], :sum + + with 5, 6r, 7.0, 8i do |init| + assert_send_type '(untyped) -> untyped', + [1, 2r, 3.0, 4i], :sum, init + end - assert_send_type "(String) { (Integer) -> String } -> String", - [1,2,3], :sum, "**" do |x| x.to_s end + assert_send_type '() { (Rational) -> untyped } -> untyped', + [1r, 2r, 3r], :sum do |ele| ele.i end + + with 5, 6r, 7.0, 8i do |init| + assert_send_type '(untyped) { (Rational) -> untyped } -> untyped', + [1r, 2r, 3r], :sum, init do |ele| ele.i end + end + + # Test an extremely esoteric edgecase, which any updates to the signature will have to allow for + outer = BlankSlate.new + def outer.+(rhs) + inner = BlankSlate.new + ::Kernel.instance_method(:instance_variable_set).bind_call(inner, :@rhs, rhs) + def inner.+(rhs) = [:innermost, @rhs, rhs] + inner + end + + assert_send_type '(untyped) -> [:innermost, 1, 2]', + [1, 2], :sum, outer end def test_take - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :take, 2 - assert_send_type "(ToInt) -> Array[Integer]", - [1,2,3], :take, ToInt.new(2) + with_int 2 do |count| + assert_send_type '(int) -> Array[Rational]', + [1r, 2r], :take, count + end end def test_take_while - assert_send_type "() { (Integer) -> bool } -> Array[Integer]", - [1,2,3], :take_while do |x| x < 2 end - assert_send_type "() -> Enumerator[Integer, Array[Integer]]", - [1,2,3], :take_while + assert_send_type '() { (Rational) -> boolish } -> Array[Rational]', + [1r, 2r, 3r], :take_while do |ele| ele <= 2 ? :truthy : nil end + assert_send_type '() -> Enumerator[Rational, Array[Rational]]', + [1r, 2r, 3r], :take_while end def test_to_a - assert_send_type "() -> Array[Integer]", - [1,2,3], :to_a + assert_send_type '() -> Array[Rational]', + [1r, 2r], :to_a + assert_send_type '() -> Array[Rational]', + ArraySubclass.new([1r, 2r]), :to_a end def test_to_ary - assert_send_type "() -> Array[Integer]", - [1,2,3], :to_ary + assert_send_type '() -> Array[Rational]', + [1r, 2r], :to_ary + assert_send_type '() -> ArrayInstanceTest::ArraySubclass[Rational]', + ArraySubclass.new([1r, 2r]), :to_ary end def test_to_h - testing "::Array[[::String, ::Integer]]" do - assert_send_type "() -> Hash[String, Integer]", - [["foo", 1], ["bar", 2]], :to_h - end + assert_send_type '() -> Hash[untyped, untyped]', + [], :to_h + assert_send_type '() -> Hash[untyped, untyped]', + [[:a, 1]], :to_h - assert_send_type "() { (Integer) -> [Symbol, String] } -> Hash[Symbol, String]", - [1,2,3], :to_h do |i| [:"s#{i}", i.to_s ] end + assert_send_type '() { (Rational) -> Hash::_Pair[Rational, Complex] } -> Hash[Rational, Complex]', + [1r, 2r], :to_h do |x| ToArray.new(x, x.i) end end def test_transpose - testing "::Array[[::String, ::Integer]]" do - assert_send_type "() -> [Array[String], Array[Integer]]", - [["foo", 1], ["bar", 2]], :transpose - end + assert_send_type '() -> Array[Array[Rational]]', + [], :transpose + assert_send_type '() -> Array[Array[Rational]]', + [[1r]], :transpose + assert_send_type '() -> Array[Array[Rational]]', + [[1r, 2r, 3r], [4r, 5r, 6r]], :transpose end def test_union - assert_send_type "() -> Array[Integer]", - [1,2,3], :union - assert_send_type "(Array[Symbol]) -> Array[Integer | Symbol]", - [1,2,3], :union, [:x, :y, :z] + assert_send_type '() -> Array[Rational]', + [1r, 2r], :union + + with_array 2r, 3r do |array1| + with_array 1, :a, 'b' do |array2| + assert_send_type '[T] (*array[T]) -> Array[Rational | T]', + [1r, 2r], :union, array1, array2 + end + end + end def test_uniq - assert_send_type "() -> Array[Integer]", - [1,2,3], :uniq - assert_send_type "() { (Integer) -> String } -> Array[Integer]", - [1,2,3], :uniq do |i| i.to_s end + assert_send_type '() -> Array[Rational]', + [1r, 2r, 3r, 2r, 1r], :uniq + assert_send_type '() { (Rational) -> Hash::_Key } -> Array[Rational]', + [1r, 2r, 3r, 2r, 1r], :uniq do |x| HashKey.new x end end def test_uniq! - assert_send_type "() -> Array[Integer]", - [1,2,3,1], :uniq! - assert_send_type "() -> nil", - [1,2,3], :uniq! - assert_send_type "() { (Integer) -> String } -> Array[Integer]", - [1,2,3, 1], :uniq! do |i| i.to_s end - assert_send_type "() { (Integer) -> String } -> nil", - [1,2,3], :uniq! do |i| i.to_s end + assert_send_type '() -> nil', + [1r, 2r, 3r], :uniq! + assert_send_type '() -> Array[Rational]', + [1r, 2r, 3r, 2r, 1r], :uniq! + assert_send_type '() { (Rational) -> Hash::_Key } -> nil', + [1r, 2r, 3r], :uniq! do |x| HashKey.new x end + assert_send_type '() { (Rational) -> Hash::_Key } -> Array[Rational]', + [1r, 2r, 3r, 2r, 1r], :uniq! do |x| HashKey.new x end + end + + def test_unshift(method: :unshift) + assert_send_type '() -> Array[Rational]', + [1r], method + assert_send_type '(*Rational) -> Array[Rational]', + [1r], method, 0r + assert_send_type '(*Rational) -> Array[Rational]', + [1r], method, -1r, 0r end - def test_unshift - assert_send_type "() -> Array[Integer]", - [1,2,3], :unshift - assert_send_type "(Integer, Integer) -> Array[Integer]", - [1,2,3], :unshift, 4, 5 + def test_prepend + test_unshift(method: :prepend) end def test_values_at - assert_send_type "() -> Array[Integer]", - [1,2,3], :values_at - assert_send_type "(Integer) -> Array[Integer]", - [1,2,3], :values_at, 2 - assert_send_type "(ToInt, Integer) -> Array[Integer?]", - [1,2,3], :values_at, ToInt.new(2), 3 - assert_send_type "(ToInt, Range[Integer]) -> Array[Integer?]", - [1,2,3], :values_at, ToInt.new(2), 0..1 + ary = 10.times.map { |x| Rational(x) } + + assert_send_type '() -> Array[Rational]', + ary, :values_at + + with_int 3 do |index1| + with_int 99999 do |index2| + with_range with_int(3).and_nil, with_int(-2).and_nil do |range| + assert_send_type '(*int | range[int?]) -> Array[Rational?]', + ary, :values_at, index1, index2, range + end + end + end end def test_zip - assert_send_type "(Array[String]) -> Array[[Integer, String?]]", - [1,2,3], :zip, ["a", "b"] - assert_send_type "(Array[String]) -> Array[[Integer, String]]", - [1,2,3], :zip, ["a", "b", "c", "d"] - assert_send_type "(Array[String], Array[Symbol]) -> Array[Array[untyped]]", - [1,2,3], :zip, ["a", "b"], [:foo, :bar] + assert_send_type '() -> Array[[Rational]]', + [1r, 2r, 3r], :zip + assert_send_type '() { ([Rational]) -> void } -> nil', + [1r, 2r, 3r], :zip do end + + with_each 1, 2 do |iter1| + assert_send_type '(_Each[Integer]) -> Array[[Rational, Integer?]]', + [1r, 2r, 3r], :zip, iter1 + assert_send_type '(_Each[Integer]) { ([Rational, Integer?]) -> void } -> nil', + [1r, 2r, 3r], :zip, iter1 do end + + with_each 1i, 2i do |iter2| + assert_send_type '(_Each[Integer], _Each[Complex]) -> Array[[Rational, Integer?, Complex?]]', + [1r, 2r, 3r], :zip, iter1, iter2 + assert_send_type '(_Each[Integer], _Each[Complex]) { ([Rational, Integer?, Complex?]) -> void } -> nil', + [1r, 2r, 3r], :zip, iter1, iter2 do end + + with_each 1.0, 2.0 do |iter3| + assert_send_type '(*_Each[Integer | Complex | Float]) -> Array[Array[Rational | Integer | Complex | Float | nil]]', + [1r, 2r, 3r], :zip, iter1, iter2, iter3 + assert_send_type '(*_Each[Integer | Complex | Float]) { (Array[Rational | Integer | Complex | Float | nil]) -> void } -> nil', + [1r, 2r, 3r], :zip, iter1, iter2, iter3 do end + end + end + end + end - assert_send_type "(Array[String]) { ([Integer, String?]) -> true } -> nil", - [1,2,3], :zip, ["a", "b"] do true end + def test_op_or + with_array 1r, 1i do |other| + assert_send_type '(array[Complex]) -> Array[Rational | Complex]', + [1r, 2r], :|, other + end + end + + def test_initialize_copy + assert_visibility :private, :initialize_copy + test_replace(method: :initialize_copy) + end - assert_send_type( - "(::_Each[String]) -> Array[[Integer, String?]]", - [1,2,3], :zip, Each.new("a", "b") - ) + def test_none? + assert_send_type '() -> bool', + [], :none? + assert_send_type '() -> bool', + [1r, 2r, 3r], :none? + assert_send_type '(singleton(Rational)) -> bool', + [1r, 2r, 3r], :none?, Rational + assert_send_type '() { (Rational) -> boolish } -> bool', + [1r, 2r, 3r], :none? do :true end + end + + def test_one? + assert_send_type '() -> bool', + [], :one? + assert_send_type '() -> bool', + [1r, 2r, 3r], :one? + assert_send_type '(singleton(Rational)) -> bool', + [1r, 2r, 3r], :one?, Rational + assert_send_type '() { (Rational) -> boolish } -> bool', + [1r, 2r, 3r], :one? do :true end + end + + def test_rassoc + with_untyped.and 1r, :a, 'b' do |object| + next unless defined? object.== + assert_send_type '(untyped) -> Array[untyped]?', + [{foo: 3}, [1r, 1i], [:a, :b, :c], ['b']], :rassoc, object + end + end + + def test_reject + test_delete_if(method: :reject) end - def test_vbar - assert_send_type "(Array[String]) -> Array[Integer | String]", - [1,2,3], :|, ["x", "y"] + def test_freeze + assert_send_type '() -> Array[Rational]', + [1r, 2r], :freeze end end diff --git a/test/stdlib/Dir_test.rb b/test/stdlib/Dir_test.rb index 80b466559d..d98130acb4 100644 --- a/test/stdlib/Dir_test.rb +++ b/test/stdlib/Dir_test.rb @@ -104,7 +104,8 @@ def test_exist? end def test_fchdir - fd = Dir.new(Dir.pwd).fileno + dir = Dir.new(Dir.pwd) + fd = dir.fileno with_int(fd) do |int| assert_send_type( @@ -117,6 +118,8 @@ def test_fchdir Dir, :fchdir, int, &proc { "string" } ) end + ensure + dir&.close end def test_foreach @@ -130,7 +133,8 @@ def test_foreach end def test_for_fd - fd = Dir.new(Dir.pwd).fileno + dir = Dir.new(Dir.pwd) + fd = dir.fileno with_int(fd) do |int| assert_send_type( @@ -138,6 +142,8 @@ def test_for_fd Dir, :for_fd, int ) end + ensure + dir&.close end def test_getwd diff --git a/test/stdlib/Etc_test.rb b/test/stdlib/Etc_test.rb index a5d73377ca..abb67b2528 100644 --- a/test/stdlib/Etc_test.rb +++ b/test/stdlib/Etc_test.rb @@ -111,3 +111,68 @@ def test_uname Etc, :uname end end + +class EtcGroupSingletonTest < Test::Unit::TestCase + include TestHelper + + library "etc" + testing "singleton(::Etc::Group)" + + def test_each + assert_send_type "() { (::Etc::Group) -> void } -> singleton(::Etc::Group)", + Etc::Group, :each do |_g| end + assert_send_type "() -> ::Enumerator[::Etc::Group]", + Etc::Group, :each + end +end + +class EtcPasswdSingletonTest < Test::Unit::TestCase + include TestHelper + + library "etc" + testing "singleton(::Etc::Passwd)" + + def test_each + assert_send_type "() { (::Etc::Passwd) -> void } -> singleton(::Etc::Passwd)", + Etc::Passwd, :each do |_u| end + assert_send_type "() -> ::Enumerator[::Etc::Passwd]", + Etc::Passwd, :each + end +end + +class EtcPasswdInstanceTest < Test::Unit::TestCase + include TestHelper + + library "etc" + testing "::Etc::Passwd" + + def test_age + omit "no age member on this platform" unless Etc::Passwd.members.include?(:age) + pw = Etc.getpwuid + + assert_send_type "() -> ::Integer", + pw, :age + assert_send_type "(::Integer) -> void", + pw, :age=, pw.age + end + + def test_comment + omit "no comment member on this platform" unless Etc::Passwd.members.include?(:comment) + pw = Etc.getpwuid + + assert_send_type "() -> ::String", + pw, :comment + assert_send_type "(::String) -> void", + pw, :comment=, pw.comment + end + + def test_quota + omit "no quota member on this platform" unless Etc::Passwd.members.include?(:quota) + pw = Etc.getpwuid + + assert_send_type "() -> ::Integer", + pw, :quota + assert_send_type "(::Integer) -> void", + pw, :quota=, pw.quota + end +end diff --git a/test/stdlib/Float_test.rb b/test/stdlib/Float_test.rb index 4981ac9e2f..f6873779ee 100644 --- a/test/stdlib/Float_test.rb +++ b/test/stdlib/Float_test.rb @@ -1,5 +1,59 @@ require_relative "test_helper" +class FloatSingletonTest < Test::Unit::TestCase + include TestHelper + + testing 'singleton(::Float)' + + def test_DIG + assert_const_type 'Integer', 'Float::DIG' + end + + def test_EPSILON + assert_const_type 'Float', 'Float::EPSILON' + end + + def test_INFINITY + assert_const_type 'Float', 'Float::INFINITY' + end + + def test_MANT_DIG + assert_const_type 'Integer', 'Float::MANT_DIG' + end + + def test_MAX + assert_const_type 'Float', 'Float::MAX' + end + + def test_MAX_10_EXP + assert_const_type 'Integer', 'Float::MAX_10_EXP' + end + + def test_MAX_EXP + assert_const_type 'Integer', 'Float::MAX_EXP' + end + + def test_MIN + assert_const_type 'Float', 'Float::MIN' + end + + def test_MIN_10_EXP + assert_const_type 'Integer', 'Float::MIN_10_EXP' + end + + def test_MIN_EXP + assert_const_type 'Integer', 'Float::MIN_EXP' + end + + def test_NAN + assert_const_type 'Float', 'Float::NAN' + end + + def test_RADIX + assert_const_type 'Integer', 'Float::RADIX' + end +end + class FloatTest < StdlibTest target Float diff --git a/test/stdlib/GC_test.rb b/test/stdlib/GC_test.rb index 04b3b89fca..dd9fdea6ed 100644 --- a/test/stdlib/GC_test.rb +++ b/test/stdlib/GC_test.rb @@ -44,12 +44,12 @@ def test_disable end def test_enable - was_enabled = GC.enable + was_disabled = GC.enable assert_send_type '() -> bool', GC, :enable ensure - GC.disable unless was_enabled + GC.disable if was_disabled end def test_start @@ -76,6 +76,10 @@ def test_stat def test_stress_and_stress= old_stress = GC.stress + # Stress mode runs a full GC on every allocation, and the assertions below allocate + # heavily while parsing and type checking. Only the types of the return values matter + # here, so keep GC disabled and let stress mode stay inert. + was_disabled = GC.disable assert_send_type '() -> (Integer | bool)', GC, :stress @@ -92,6 +96,7 @@ def test_stress_and_stress= end ensure GC.stress = old_stress + GC.enable unless was_disabled end def test_total_time diff --git a/test/stdlib/Hash_test.rb b/test/stdlib/Hash_test.rb index 5f046de2c8..14e1ed8b71 100644 --- a/test/stdlib/Hash_test.rb +++ b/test/stdlib/Hash_test.rb @@ -1,438 +1,718 @@ require_relative "test_helper" -class HashTest < StdlibTest - target Hash +class HashSingletonTest < Test::Unit::TestCase + include TestHelper - # Hash[] - def test_singleton_aref - Hash[a: 42, b: 43] - Hash[[[:a, 1], [:b, 3]]] - Hash[:a, 1, :b, 3] - end + testing 'singleton(Hash)' + + Subclass = Class.new(Hash) + + def test_op_aref + with_hash 'hello' => :word do |hash| + assert_send_type '(hash[String, Symbol]) -> HashSingletonTest::Subclass[String, Symbol]', + Subclass, :[], hash + end - def test_singleton_try_convert - Hash.try_convert({foo: 1}) - Hash.try_convert(nil) - Hash.try_convert(42) + with_array :a, 1 do |pair1| + with_array :b, 2 do |pair2| + with_array pair1, pair2 do |ary| + assert_send_type '(array[Hash::_Pair[String, Symbol]]) -> HashSingletonTest::Subclass[Symbol, Integer]', + Subclass, :[], ary + end + end + end + + assert_send_type '(*String | Integer) -> Hash[String | Integer, String | Integer]', + Subclass, :[], 'a', 1, 'b', 2 end - # test_< - def test_less_than - _ = { a: 1 } < { a: 1, b: 2 } - _ = { a: 1 } < { 'a' => '1' } + def test_try_convert + with_hash 'hello' => :word do |hash| + assert_send_type '(hash[String, Symbol]) -> Hash[String, Symbol]', + Hash, :try_convert, hash + end + + with_untyped do |untyped| + next if defined? untyped.to_hash + + assert_send_type '(untyped) -> nil', + Hash, :try_convert, untyped + end end - # test_<= - def test_less_than_equal - _ = { a: 1 } <= { a: 1, b: 2 } - _ = { a: 1 } <= { 'a' => '1' } + def test_new + assert_send_type '() -> Hash[Symbol, Rational]', + Hash, :new + assert_send_type '(Rational) -> Hash[Symbol, Rational]', + Hash, :new, 1r + assert_send_type '() { (Hash[Symbol, Rational], Hash::_Key) -> Rational } -> Hash[Symbol, Rational]', + Hash, :new do 1r end + + with_int 1 do |capacity| + assert_send_type '(capacity: int) -> Hash[Symbol, Rational]', + Hash, :new, capacity: capacity + assert_send_type '(Rational, capacity: int) -> Hash[Symbol, Rational]', + Hash, :new, 1r, capacity: capacity + assert_send_type '(capacity: int) { (Hash[Symbol, Rational], Hash::_Key) -> Rational } -> Hash[Symbol, Rational]', + Hash, :new, capacity: capacity do 1r end + end end +end + +class HashInstanceTest < Test::Unit::TestCase + include TestHelper + + testing 'Hash[Symbol, Rational]' - def test_eqeq - _ = { a: 1 } == { a: 1 } - _ = { a: 1 } == { b: 2 } + class BlankKey < BlankSlate + def initialize(key) = @key = key + def hash = @key.hash + def eql?(rhs) = @key.eql?(rhs) end - # test_> - def test_greater_than - _ = { a: 1 } > { a: 1, b: 2 } - _ = { a: 1 } > { a: 1, b: '2' } + def with_key(key) + yield key + yield BlankKey.new key end - # test_>= - def test_greater_than_equal - _ = { a: 1 } >= { a: 1, b: 2 } - _ = { a: 1 } >= { ab: 1, b: 2 } + def test_op_lt + with_hash a: 3, 'b' => 4 do |hash| + assert_send_type '(hash[untyped, untyped]) -> bool', + {a: 1r}, :<, hash + end end - def test_aref - { foo: 1 }[:foo] - { foo: 1 }[1] + def test_op_le + with_hash a: 3, 'b' => 4 do |hash| + assert_send_type '(hash[untyped, untyped]) -> bool', + {a: 1r}, :<=, hash + end end - def test_aset - hash = { foo: 1 } - hash[:a] = 2 - hash.store(:b, 3) + def test_op_gt + with_hash a: 3, 'b' => 4 do |hash| + assert_send_type '(hash[untyped, untyped]) -> bool', + {a: 1r}, :>, hash + end end - def test_any? - { foo: 1, bar: 2 }.any? - { foo: 1, bar: 2 }.any?(Array) - # Pending because I tried to pass the test but I can't - # { foo: 1, bar: 2 }.any? { |k, v| k == :foo && v == 1 } + def test_op_ge + with_hash a: 3, 'b' => 4 do |hash| + assert_send_type '(hash[untyped, untyped]) -> bool', + {a: 1r}, :>=, hash + end end - def test_assoc - { foo: 1, bar: 2 }.assoc(:foo) - { foo: 1, bar: 2 }.assoc(:baz) + def test_rehash + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r}, :rehash end - def test_clear - { foo: 1, bar: 2 }.clear + def test_freeze + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r}, :freeze end - def test_compact - { a: nil }.compact - { a: nil, b: 2 }.compact - { b: 2 }.compact - Class.new(Hash)[:a, nil].compact + def test_to_hash + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r}, :to_hash end - def test_compact! - { a: nil }.compact! - { a: 1 }.compact! + def test_to_a + assert_send_type '() -> Array[[Symbol, Rational]]', + {a: 1r}, :to_a end - def test_compare_by_identity - { a: 1 }.compare_by_identity + def test_to_h + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r}, :to_h + + with_array 'a', 1i do |pair| + assert_send_type '[A, B] () { (Symbol, Rational) -> Hash::_Pair[A, B] } -> Hash[A, B]', + {a: 1r}, :to_h do pair end + end end - def test_compare_by_identity? - hash = { a: 1 } - hash.compare_by_identity? - hash.compare_by_identity - hash.compare_by_identity? + def test_inspect(method: :inspect) + assert_send_type '() -> String', + {a: 1r}, method end - def test_deconstruct_keys - { a: 1 }.deconstruct_keys([]) - { a: 1 }.deconstruct_keys([:a]) - { a: 1 }.deconstruct_keys(nil) + def test_to_s + test_inspect(method: :to_s) end - # default and default= - def test_default - hash = {} - hash.default - hash.default = 1 - hash.default + def test_to_proc + assert_send_type '() -> ^(Symbol) -> Rational?', + {a: 1r}, :to_proc + + # Make sure return type is correct + assert_type 'Rational', {a: 1r}.to_proc.call(:a) + assert_type 'nil', {a: 1r}.to_proc.call(:not_a_key) + + # make sure it only takes one argument + assert_raises ArgumentError do {a: 1r}.to_proc.call(:a, :b) end + assert_raises ArgumentError do {a: 1r}.to_proc.call() end end - # default_proc and default_proc= - def test_default_proc - hash = {} - hash.default_proc - hash.default_proc = proc { |h, k| k } - hash.default_proc - hash.default_proc = nil - hash.default_proc - hash.default_proc = :key - hash.default_proc + def test_op_eq + h = {a: 1r} + with_untyped.and h do |untyped| + assert_send_type '(untyped) -> bool', + h, :==, untyped + end end - def test_delete - hash = { a: 123 } - hash.delete(:a) - hash.delete(:z) - hash.delete(:z) { |k| "#{k} not found" } + def test_op_aref + with_key :a do |key| + assert_send_type '(Hash::_Key) -> Rational', + {a: 1r}, :[], key + end + + with_key :not_a_key do |key| + assert_send_type '(Hash::_Key) -> nil', + {a: 1r}, :[], key + end end - def test_delete_if - hash = { a: 123 } - hash.delete_if - hash.delete_if { |k, v| k == :a && v == 123 } + def test_hash + assert_send_type '() -> Integer', + {a: 1r}, :hash end - def test_dig - hash = { a: 123, b: { foo: 1 } } - hash.dig(:a) - hash.dig(:b, :foo) + def test_eql? + h = {a: 1r} + with_untyped.and h do |untyped| + assert_send_type '(untyped) -> bool', + h, :eql?, untyped + end end - def test_each - h = { a: 123 } + def test_fetch + with_key :a do |key| + assert_send_type '(Hash::_Key) -> Rational', + {a: 1r}, :fetch, key + + with_untyped do |default_value| + assert_send_type '(Hash::_Key, untyped) -> Rational', + {a: 1r}, :fetch, key, default_value + end - h.each do |k, v| - # nop + assert_send_type '[K2 < Hash::_Key] (K2) {(K2) -> void} -> Rational', + {a: 1r}, :fetch, key do fail end end - h.each do |x| - # nop - end + with_key :not_a_key do |key| + with_untyped do |default_value| + assert_send_type '[T] (Hash::_Key, T) -> T', + {a: 1r}, :fetch, key, default_value - h.each.each do |x, y| - # + assert_send_type '[K2 < Hash::_Key, T] (K2) { (K2) -> T } -> T', + {a: 1r}, :fetch, key do default_value end + end end end - def test_each_key - h = { a: 123 } + def test_op_aset(method: :[]=) + assert_send_type '(Symbol, Rational) -> Rational', + {a: 1r}, method, :a, 10r + assert_send_type '(Symbol, Rational) -> Rational', + {a: 1r}, method, :not_a_key, 10r + end - h.each_key do |k| - # nop + def test_store + test_op_aset(method: :store) + end + + def test_default + no_default = {a: 10r} + with_default = Hash.new(10r) + + assert_send_type '() -> nil', + no_default, :default + assert_send_type '() -> Rational', + with_default, :default + + with_key :a do |key| + assert_send_type '(Hash::_Key) -> nil', + no_default, :default, key + assert_send_type '(Hash::_Key) -> Rational', + with_default, :default, key end + end - h.each_key + def test_default= + assert_send_type '(Rational) -> Rational', + {a: 1r}, :default=, 1r end - def test_each_value - h = { a: 123 } + def test_default_proc + assert_send_type '() -> nil', + {a: 1r}, :default_proc + assert_send_type '() -> ^(HashInstanceTest::Subclass[Symbol, Rational], Hash::_Key) -> Rational', + Subclass.new{ :oops }, :default_proc + end + + Subclass = Class.new(Hash) + + def test_default_proc= + assert_send_type '(nil) -> nil', + {}, :default_proc=, nil + assert_send_type '(^(HashInstanceTest::Subclass[Symbol, Rational], Hash::_Key) -> Rational) -> ^(HashInstanceTest::Subclass[Symbol, Rational], Hash::_Key) -> Rational', + Subclass[], :default_proc=, ->(h, k) { 1r } - h.each_value do |v| - # nop + toproc = BlankSlate.new + def toproc.to_proc = ->(h, k) { 1r } + with proc{}, toproc do |proc_like| + assert_send_type '(_ToProc) -> Proc', + {}, :default_proc=, proc_like end + end + + def test_key + equals_1r = BlankSlate.new + def equals_1r.==(r) = 1r == r - h.each_value + assert_send_type '(Hash::_Equals) -> Symbol', + { a: 1r, b: 2r }, :key, equals_1r + assert_send_type '(Hash::_Equals) -> nil', + { a: 3r, b: 2r }, :key, equals_1r + end + + def test_size(method: :size) + assert_send_type '() -> Integer', + {a: 1r}, method + end + + def test_length + test_size(method: :length) end def test_empty? - {}.empty? - { a: 1 }.empty? + assert_send_type '() -> bool', + {a: 1r}, :empty? end - def test_eql? - { a: 1 }.eql?({ a: 1 }) - { a: 2 }.eql?({ a: 1 }) + def test_each_value + assert_send_type '() -> Enumerator[Rational, Hash[Symbol, Rational]]', + {a: 1r}, :each_value + assert_send_type '() { (Rational) -> void } -> Hash[Symbol, Rational]', + {a: 1r}, :each_value do end end - def test_except - omit_if(!Hash.method_defined?(:except)) - { a: 100, b: 200, c: 300 }.except(:a) - { a: 100, b: 200, c: 300 }.except(:b, :c, :d) + def test_each_key + assert_send_type '() -> Enumerator[Symbol, Hash[Symbol, Rational]]', + {a: 1r}, :each_key + assert_send_type '() { (Symbol) -> void } -> Hash[Symbol, Rational]', + {a: 1r}, :each_key do end end - def test_fetch - hash = { a: 1 } - hash.fetch(:a) - hash.fetch(:b, 2) - hash.fetch(:b) { |key| key } + def test_each(method: :each) + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]]', + {a: 1r}, method + assert_send_type '() { ([Symbol, Rational]) -> void } -> Hash[Symbol, Rational]', + {a: 1r}, method do end end - def test_fetch_values - hash = { a: 1, b: 42 } - hash.fetch_values(:a) - hash.fetch_values(:a, :b) - hash.fetch_values(:unknown) { |key| key } + def test_each_pair + test_each(method: :each_pair) end - def test_filter - { a: 1, b: 2 }.filter - { a: 1, b: 2 }.filter { |k, v| v == 1 } - Class.new(Hash)[:a, nil].filter.each { |k, v| k } - Class.new(Hash)[:a, nil].filter { |k, v| k } + def test_transform_keys + assert_send_type '() -> Enumerator[Symbol, Hash[untyped, Rational]]', + {a: 1r}, :transform_keys + + with_hash a: 'hello', 'not_a_symbol' => 'what' do |replacements| + assert_send_type '(hash[Hash::_Key, String]) -> Hash[Symbol | String, Rational]', + {a: 1r}, :transform_keys, replacements + assert_send_type '(hash[Hash::_Key, String]) { (Symbol) -> String } -> Hash[String, Rational]', + {a: 1r}, :transform_keys, replacements, &:to_s + end - { a: 1, b: 2 }.select - { a: 1, b: 2 }.select { |k, v| v == 1 } - Class.new(Hash)[:a, nil].select.each { |k, v| k } - Class.new(Hash)[:a, nil].select { |k, v| k } + assert_send_type '() { (Symbol) -> String } -> Hash[String, Rational]', + {a: 1r}, :transform_keys, &:to_s end - def test_filter! - { a: 1 }.filter! - { a: 1 }.filter! { |k, v| v == 0 } - { a: 1 }.filter! { |k, v| v == 1 } + def test_transform_keys! + ## You cannot properly test the following, because when `assert_send_type` tries to figure out + ## the types, it ends up converting the hash to `{nil => 1r}`. + # assert_send_type '() -> Enumerator[Symbol, Hash[Symbol, Rational]]', + # {a: 1r}, :transform_keys! + + with_hash a: :hello, 'not_a_symbol' => :what do |replacements| + assert_send_type '(hash[Hash::_Key, Symbol]) -> Hash[Symbol, Rational]', + {a: 1r}, :transform_keys!, replacements + assert_send_type '(hash[Hash::_Key, Symbol]) { (Symbol) -> Symbol } -> Hash[Symbol, Rational]', + {a: 1r}, :transform_keys!, replacements, &:upcase + end - { a: 1 }.select! - { a: 1 }.select! { |k, v| v == 0 } - { a: 1 }.select! { |k, v| v == 1 } + assert_send_type '() { (Symbol) -> Symbol } -> Hash[Symbol, Rational]', + {a: 1r}, :transform_keys!, &:upcase end - def test_flatten - h = { a: 1, b: 2, c: [3, 4, 5] } - h.flatten - h.flatten(1) - h.flatten(2) + def test_transform_values + assert_send_type '() -> Enumerator[Rational, Hash[Symbol, untyped]]', + {a: 1r}, :transform_values + assert_send_type '() { (Rational) -> Complex } -> Hash[Symbol, Complex]', + {a: 1r}, :transform_values, &:i end - def test_has_key? - h = { a: 1, b: 42 } - h.has_key?(:a) - h.has_key?(:x) - h.include?(:b) - h.key?(:c) - h.member?(:x) + def test_transform_values! + ## You cannot properly test the following, because when `assert_send_type` tries to figure out + ## the types, it ends up converting the hash to `{a: nil}`. + # assert_send_type '() -> Enumerator[Rational, Hash[Symbol, Rational]]', + # {a: 1r}, :transform_values! + assert_send_type '() { (Rational) -> Rational } -> Hash[Symbol, Rational]', + {a: 1r}, :transform_values! do |rat| rat + 1 end end - def test_has_value? - h = { a: 1, b: 42 } - h.has_value?(42) - h.has_value?(2) - h.value?(42) - h.value?(2) + def test_keys + assert_send_type '() -> Array[Symbol]', + {a: 1r}, :keys end - def test_hash - { a: 1 }.hash + def test_values + assert_send_type '() -> Array[Rational]', + {a: 1r}, :values end - def test_inspect - { a: 1 }.inspect - { a: 1 }.to_s + def test_values_at + assert_send_type '() -> Array[Rational]', + {a: 1r}, :values_at + + with_key :a do |key| + assert_send_type '(*Hash::_Key) -> Array[Rational]', + {a: 1r}, :values_at, key + + with_key :not_a_key do |not_a_key| + assert_send_type '(*Hash::_Key) -> Array[Rational?]', + {a: 1r}, :values_at, key, not_a_key + end + end end - def test_invert - { a: 1, b: 42 }.invert + def test_fetch_values + assert_send_type '() -> Array[Rational]', + {a: 1r}, :fetch_values + + with_key :a do |key| + assert_send_type '(*Hash::_Key) -> Array[Rational]', + {a: 1r}, :fetch_values, key + assert_send_type '[K < Hash::_Key] (*K) { (K) -> Complex } -> Array[Rational]', + {a: 1r}, :fetch_values, key do 1i end + + with_key :not_a_key do |not_a_key| + assert_send_type '[K < Hash::_Key] (*K) { (K) -> Complex } -> Array[Rational | Complex]', + {a: 1r}, :fetch_values, key, not_a_key do 1i end + end + end end - def test_keep_if - hash = { a: 1, b: 2 } - hash.keep_if - hash.keep_if { |k, v| k == :a } + def test_shift + assert_send_type '() -> nil', + {}, :shift + assert_send_type '() -> [Symbol, Rational]', + {a: 1r}, :shift end - def test_key - hash = { a: 1 } - hash.key(1) - hash.key(3) + def test_delete + with_key :a do |key| + assert_send_type '(Hash::_Key) -> nil', + {}, :delete, key + assert_send_type '(Hash::_Key) -> Rational', + {a: 1r}, :delete, key + + assert_send_type '[K < Hash::_Key] (K) { (K) -> Complex } -> (Rational | Complex)', + {}, :delete, key do 1i end + assert_send_type '[K < Hash::_Key] (K) { (K) -> Complex } -> (Rational | Complex)', + {a: 1r}, :delete, key do 1i end + end end - def test_keys - { a: 1, b: 2 }.keys + def test_delete_if + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]]', + {a: 1r}, :delete_if + assert_send_type '() { (Symbol, Rational) -> boolish } -> Hash[Symbol, Rational]', + {a: 1r}, :delete_if do :boolish end end - def test_length - { a: 1, b: 2 }.length - { a: 1, b: 2 }.size + def test_keep_if + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]]', + {a: 1r}, :keep_if + assert_send_type '() { (Symbol, Rational) -> boolish } -> Hash[Symbol, Rational]', + {a: 1r}, :keep_if do :boolish end end - def test_merge - hash = { a: 1, b: 2 } - hash.merge({ 'k' => 'v' }) - hash.merge({ a: 3 }) { |k, v1, v2| [v1, v2] } + def test_select(method: :select) + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]]', + {a: 1r}, method + assert_send_type '() { (Symbol, Rational) -> boolish } -> Hash[Symbol, Rational]', + {a: 1r}, method do :boolish end end - def test_merge! - hash = { a: 1, b: 2 } - hash.merge!({ 'k' => 'v' }) - hash.merge!({ a: 3 }) { |k, v1, v2| [v1, v2] } - hash.update({ 'foo' => 42 }) + def test_select!(method: :select!) + assert_send_type '() -> Enumerator[[Symbol, Rational], nil]', + {}, method + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]]', + {a: 1r}, method + assert_send_type '() { (Symbol, Rational) -> boolish } -> nil', + {a: 1r}, method do :boolish end + assert_send_type '() { (Symbol, Rational) -> boolish } -> Hash[Symbol, Rational]', + {a: 1r}, method do nil end end - def test_rassoc - hash = { a: 1, b: 2 } - hash.rassoc(2) - hash.rassoc(42) + def test_filter + test_select(method: :filter) end - def test_rehash - { a: 1, b: 2 }.rehash + def test_filter! + test_select!(method: :filter!) end def test_reject - hash = { a: 1, b: 2 } - hash.reject - hash.reject { |k, v| k == :a } + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]]', + {a: 1r}, :reject + assert_send_type '() { (Symbol, Rational) -> boolish } -> Hash[Symbol, Rational]', + {a: 1r}, :reject do :boolish end end def test_reject! - hash = { a: 1, b: 2 } - hash.reject! - hash.reject! { |k, v| k == :a } - hash.reject! { |k, v| k == :a } + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]?]', + {}, :reject! + assert_send_type '() -> Enumerator[[Symbol, Rational], Hash[Symbol, Rational]?]', + {a: 1r}, :reject! + assert_send_type '() { (Symbol, Rational) -> boolish } -> Hash[Symbol, Rational]', + {a: 1r}, :reject! do :boolish end + assert_send_type '() { (Symbol, Rational) -> boolish } -> nil', + {a: 1r}, :reject! do nil end end - def test_replace - { a: 1 }.replace({ b: 2 }) - { a: 1 }.replace({ 'b' => 2.0 }) + def test_slice + assert_send_type '[K < Hash::_Key] (*K) -> Hash[K, Rational]', + {a: 1r}, :slice + with_key :a do |key| + assert_send_type '[K < Hash::_Key] (*K) -> Hash[K, Rational]', + {a: 1r}, :slice, key + with_key :not_a_key do |not_a_key| + assert_send_type '[K < Hash::_Key] (*K) -> Hash[K, Rational]', + {a: 1r}, :slice, key, not_a_key + end + end end - def test_shift - {}.shift - { a: 42 }.shift + def test_except + assert_send_type '(*Hash::_Key) -> Hash[Symbol, Rational]', + {a: 1r, b: 2r}, :except + + with_key :a do |key| + assert_send_type '(*Hash::_Key) -> Hash[Symbol, Rational]', + {a: 1r, b: 2r}, :except, key + + with_key :not_a_key do |not_a_key| + assert_send_type '(*Hash::_Key) -> Hash[Symbol, Rational]', + {a: 1r, b: 2r}, :except, key, not_a_key + end + end end - def test_slice - { a: 42, b: 43, c: 44 }.slice(:a) - { a: 42, b: 43, c: 44 }.slice(:a, :b) - Class.new(Hash)[:a, 42].slice(:a) + def test_clear + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r, b: 2r}, :clear end - def test_to_a - { a: 42 }.to_a + def test_invert + assert_send_type '() -> Hash[Rational, Symbol]', + {a: 1r, b: 2r}, :invert end - def test_to_h - { a: 42 }.to_h - { a: 42 }.to_h { |k, v| [k.to_s, v.to_f] } + def test_update + test_merge!(method: :update) end - def test_to_hash - { a: 42 }.to_hash - Class.new(Hash)[:a, 42].to_hash + def test_replace(method: :replace) + with_hash a: 2r do |hash| + assert_send_type '(hash[Symbol, Rational]) -> Hash[Symbol, Rational]', + {a: 1r}, method, hash + end end - def test_to_proc - { a: 1 }.to_proc.call(:a) - { a: 1 }.to_proc.call(:b) + def test_initialize_copy + test_replace(method: :initialize_copy) end - def test_transform_keys - { a: 1, b: 2 }.transform_keys - { a: 1, b: 2 }.transform_keys(&:to_s) + def test_merge!(method: :merge!) + assert_send_type '(*hash[Symbol, Rational]) -> Hash[Symbol, Rational]', + {a: 1r}, method + assert_send_type '(*hash[Symbol, Rational]) {(Symbol, Rational, Rational) -> Rational} -> Hash[Symbol, Rational]', + {a: 1r}, method do 2r end + + with_hash b: 2r do |hash1| + assert_send_type '(*hash[Symbol, Rational]) -> Hash[Symbol, Rational]', + {a: 1r}, method, hash1 + assert_send_type '(*hash[Symbol, Rational]) {(Symbol, Rational, Rational) -> Rational} -> Hash[Symbol, Rational]', + {a: 1r}, method, hash1 do 2r end + + with_hash a: 3r do |hash2| + assert_send_type '(*hash[Symbol, Rational]) -> Hash[Symbol, Rational]', + {a: 1r}, method, hash1, hash2 + assert_send_type '(*hash[Symbol, Rational]) {(Symbol, Rational, Rational) -> Rational} -> Hash[Symbol, Rational]', + {a: 1r}, method, hash1, hash2 do 2r end + end + end end - def test_transform_keys! - { a: 1, b: 2 }.transform_keys! - { a: 1, b: 2 }.transform_keys!(&:to_s) + def test_merge + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r}, :merge + assert_send_type '() { (Symbol, Rational, Complex) -> Float} -> Hash[Symbol, Rational]', + {a: 1r}, :merge do 1.0 end + + with_hash 'b' => 2i do |hash1| + assert_send_type '(*hash[String, Complex]) -> Hash[Symbol | String, Rational | Complex]', + {a: 1r}, :merge, hash1 + assert_send_type '(*hash[String, Complex]) { (Symbol, Rational, Complex) -> Float} -> Hash[Symbol | String, Rational | Complex]', + {a: 1r}, :merge, hash1 do 1.0 end + + with_hash a: 3i do |hash2| + assert_send_type '(*hash[String, Complex]) -> Hash[Symbol | String, Rational | Complex]', + {a: 1r}, :merge, hash1, hash2 + assert_send_type '(*hash[String, Complex]) { (Symbol, Rational, Complex) -> Float} -> Hash[Symbol | String, Rational | Complex | Float]', + {a: 1r}, :merge, hash1, hash2 do 1.0 end + end + end end - def test_transform_values - { a: 1, b: 2 }.transform_values - { a: 1, b: 2 }.transform_values(&:to_s) + def test_assoc + equals_symbol_a = BlankSlate.new + def equals_symbol_a.==(r) = :a == r + + with :a, equals_symbol_a do |key| + assert_send_type '(Hash::_Equals) -> [Symbol, Rational]', + {a: 1r}, :assoc, key + assert_send_type '(Hash::_Equals) -> nil', + {b: 1r}, :assoc, key + end end - def test_transform_values! - { a: 1, b: 2 }.transform_values! - { a: 1, b: 2 }.transform_values!(&:to_s) + def test_rassoc + equals_1r = BlankSlate.new + def equals_1r.==(r) = 1r == r + + with 1r, equals_1r do |value| + assert_send_type '(Hash::_Equals) -> [Symbol, Rational]', + {a: 1r}, :rassoc, value + assert_send_type '(Hash::_Equals) -> nil', + {a: 2r}, :rassoc, value + end end - def test_values - { a: 1, b: 2 }.values + def test_flatten + assert_send_type '() -> Array[Symbol | Rational]', + {a: 1r}, :flatten + assert_send_type '(1) -> Array[Symbol | Rational]', + {a: 1r}, :flatten, 1 + assert_send_type '(0) -> Array[[Symbol, Rational]]', + {a: 1r}, :flatten, 0 + + with_int -1 do |level| + assert_send_type '(int) -> Array[untyped]', + {a: 1r}, :flatten, level + end end - def test_values_at - { a: 1, b: 2, c: 3 }.values_at(:a, :b, :d) + def test_compact + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r, b: nil}, :compact end -end -class HashInstanceTest < Test::Unit::TestCase - include TestHelper + def test_compact! + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r, b: nil}, :compact! + assert_send_type '() -> nil', + {a: 1r, b: 2r}, :compact! + end - testing "::Hash[::Symbol, ::Integer]" + def test_include? + test_has_key?(method: :include?) + end - def test_except - assert_send_type "() -> ::Hash[::Symbol, ::Integer]", - { a: 100, b: 200, c: 300 }, :except - assert_send_type "(*Symbol keys) -> ::Hash[::Symbol, ::Integer]", - { a: 100, b: 200, c: 300 }, :except, :a + def test_member? + test_has_key?(method: :member?) end -end -class HashSingletonTest < Test::Unit::TestCase - include TestHelper + def test_has_key?(method: :has_key?) + with_key :a do |key| + assert_send_type '(Hash::_Key) -> bool', + {a: 1r}, method, key + assert_send_type '(Hash::_Key) -> bool', + {b: 1r}, method, key + end + end - testing "singleton(::Hash)" + def test_has_value?(method: :has_value?) + equals_1r = BlankSlate.new + def equals_1r.==(r) = 1r == r - def test_new - assert_send_type( - "() -> ::Hash[untyped, untyped]", - Hash, :new - ) - assert_send_type( - "(String) -> ::Hash[untyped, String]", - Hash, :new, "default" - ) - assert_send_type( - "() { (Hash[untyped, untyped], untyped) -> untyped } -> ::Hash[untyped, String]", - Hash, :new, &-> (hash, key) { } - ) - - if_ruby("3.4"..., skip: false) do - with_int(1) do |int| - assert_send_type( - "(capacity: int) -> ::Hash[untyped, untyped]", - Hash, :new, capacity: int - ) - assert_send_type( - "(String, capacity: int) -> ::Hash[untyped, String]", - Hash, :new, "default", capacity: int - ) - assert_send_type( - "(capacity: int) { (Hash[untyped, untyped], untyped) -> untyped } -> ::Hash[untyped, String]", - Hash, :new, capacity: int, &-> (hash, key) { } - ) - end + with 1r, equals_1r do |value| + assert_send_type '(Hash::_Equals) -> bool', + {a: 1r}, method, value + assert_send_type '(Hash::_Equals) -> bool', + {a: 12}, method, value + end + end + + def test_key? + test_has_key?(method: :key?) + end + + def test_value? + test_has_value?(method: :value?) + end + + def test_compare_by_identity + assert_send_type '() -> Hash[Symbol, Rational]', + {a: 1r}, :compare_by_identity + end + + def test_compare_by_identity? + assert_send_type '() -> bool', + {a: 1r}, :compare_by_identity? + assert_send_type '() -> bool', + {a: 1r}.compare_by_identity, :compare_by_identity? + end + + def test_any? + assert_send_type '() -> bool', + {}, :any? + assert_send_type '() -> bool', + {a: 1r}, :any? + + pattern = BlankSlate.new + def pattern.===(r) = r == [:a, 1r] ? :boolish : nil + assert_send_type '(Enumerable::_Pattern) -> bool', + {a: 1r}, :any?, pattern + assert_send_type '(Enumerable::_Pattern) -> bool', + {a: 2r}, :any?, pattern + + assert_send_type '() { ([Symbol, Rational]) -> boolish } -> bool', + {a: 1r}, :any? do :boolish end + assert_send_type '() { ([Symbol, Rational]) -> boolish } -> bool', + {a: 1r}, :any? do nil end + end + + def test_dig + with_key :a do |key| + assert_send_type '(Hash::_Key, *untyped) -> untyped', + {a: [1r]}, :dig, key, 0 + assert_send_type '(Hash::_Key, *untyped) -> untyped', + {b: [1r]}, :dig, key, 0 + end + end + + def test_deconstruct_keys + with_untyped do |argument| + assert_send_type '(untyped) -> Hash[Symbol, Rational]', + {a: 1r}, :deconstruct_keys, argument end end end diff --git a/test/stdlib/IO_test.rb b/test/stdlib/IO_test.rb index e2b1971947..453256ddc7 100644 --- a/test/stdlib/IO_test.rb +++ b/test/stdlib/IO_test.rb @@ -1,5 +1,6 @@ require_relative "test_helper" require 'tempfile' +require 'etc' require "io/wait" @@ -227,6 +228,15 @@ def test_append_symbol end end + def test_pathconf + IO.pipe do |r, w| + assert_send_type "(Integer) -> Integer", + w, :pathconf, Etc::PC_PIPE_BUF + end + rescue NotImplementedError + omit "Not implemented" + end + def test_advise IO.open(IO.sysopen(File.expand_path(__FILE__))) do |io| assert_send_type "(Symbol) -> nil", @@ -327,10 +337,14 @@ def test_read io, :read, 1 assert_send_type "(nil) -> String", io, :read, nil + assert_send_type "(nil, nil) -> String", + io, :read, nil, nil assert_send_type "(Integer, String) -> String", io, :read, 0, +"buffer" assert_send_type "(Integer, String) -> nil", io, :read, 1, +"buffer" + assert_send_type "(Integer, nil) -> String?", + io, :read, 0, nil assert_send_type "(nil, String) -> String", io, :read, nil, +"buffer" end @@ -340,11 +354,41 @@ def test_readpartial IO.open(IO.sysopen(File.expand_path(__FILE__))) do |io| assert_send_type "(Integer) -> String", io, :readpartial, 10 + assert_send_type "(Integer, nil) -> String", + io, :readpartial, 10, nil assert_send_type "(Integer, String) -> String", io, :readpartial, 10, +"buffer" end end + def test_read_nonblock + IO.open(IO.sysopen(File.expand_path(__FILE__))) do |io| + assert_send_type "(Integer) -> String", + io, :read_nonblock, 10 + assert_send_type "(Integer, nil) -> String", + io, :read_nonblock, 10, nil + assert_send_type "(Integer, String) -> String", + io, :read_nonblock, 10, +"buffer" + end + end + + def test_sysread + IO.open(IO.sysopen(File.expand_path(__FILE__))) do |io| + assert_send_type "(Integer) -> String", + io, :sysread, 1 + end + + IO.open(IO.sysopen(File.expand_path(__FILE__))) do |io| + assert_send_type "(Integer, nil) -> String", + io, :sysread, 1, nil + end + + IO.open(IO.sysopen(File.expand_path(__FILE__))) do |io| + assert_send_type "(Integer, String) -> String", + io, :sysread, 1, +"buffer" + end + end + def test_timeout io, _ = IO.pipe() @@ -506,6 +550,10 @@ def test_pread "(int, int) -> String", io, :pread, maxlen, offset ) + assert_send_type( + "(int, int, nil) -> String", + io, :pread, maxlen, offset, nil + ) with_string(+"buffer") do |buffer| assert_send_type( "(int, int, string) -> String", diff --git a/test/stdlib/Integer_test.rb b/test/stdlib/Integer_test.rb index 79d4d1c7f9..db26b2e1f3 100644 --- a/test/stdlib/Integer_test.rb +++ b/test/stdlib/Integer_test.rb @@ -1,385 +1,407 @@ -require_relative "test_helper" +require_relative 'test_helper' -class IntegerTest < StdlibTest - target Integer +class IntegerSingletonTest < Test::Unit::TestCase + include TestHelper + + testing 'singleton(Integer)' def test_sqrt - Integer.sqrt(4) - Integer.sqrt(4.0) - Integer.sqrt(4/1r) - Integer.sqrt(ToInt.new) + with_int 38 do |int| + assert_send_type '(int) -> Integer', + Integer, :sqrt, int + end end - def test_modulo - 3 % 1 - 3 % 1.1 - 3 % 1.5r - end - - def test_bitwise_ops - 3 & 1 - 1 ^ 3 - end - - def test_calc - 3 * 1 - 3 * 1.0 - 3 * (1r/3) - - 3 - 1 - 3 - 0.1 - 3 - 1r - 3 - 1.to_c - - 1 ** 1 - 2 ** 2.1 - 3 ** 3r - 3 ** 10.to_c - - 3 + 1 - 3 + 1.0 - 3 + (1r/3) - 3 + 10.to_c - - 3 / 1 - 3 / 1.0 - 3 / (1r/3) - 30 / 10.to_c - end - - def test_compare - 3 < 1 - 3 < 1.0 - 3 < 1r - - 1 > 1 - 1 > 1.0 - 1 > 1r - - 1 <= 3 - 1 <= 1.3 - 1 <= 3r - - 1 >= 3 - 1 >= 1.3 - 1 >= 3r - - 1 <=> 1 - 1 <=> 1.0 - 1 <=> 3r - - 3 === 3.0 - 3 === "" - end - - def test_shift - 1 << 30 - 1 << 30.to_f - 1 << ToInt.new - - 1 >> 30 - 1 >> 30.to_f - 1 >> ToInt.new - end - - def test_aref - 3[0] - 3[0.3] - 3[ToInt.new] - - 3[1,2] - 3[1...3] - end - - def test_to_s - 1.to_s - 1.to_s(2) - 1.to_s(3) - 1.to_s(4) - 1.to_s(5) - 1.to_s(6) - 1.to_s(7) - 1.to_s(8) - 1.to_s(9) - 1.to_s(10) - 1.to_s(11) - 1.to_s(12) - 1.to_s(13) - 1.to_s(14) - 1.to_s(15) - 1.to_s(16) - 1.to_s(17) - 1.to_s(18) - 1.to_s(19) - 1.to_s(20) - 1.to_s(21) - 1.to_s(22) - 1.to_s(23) - 1.to_s(24) - 1.to_s(25) - 1.to_s(26) - 1.to_s(27) - 1.to_s(28) - 1.to_s(29) - 1.to_s(30) - 1.to_s(31) - 1.to_s(32) - 1.to_s(33) - 1.to_s(34) - 1.to_s(35) - 1.to_s(36) - 30.to_s(ToInt.new) - end - - def test_abs_abs2 - 3.abs - 3.abs2 + def test_try_convert + with_int 1 do |int| + assert_send_type '(int) -> Integer', + Integer, :try_convert, int + end + + with_untyped do |untyped| + assert_send_type '(untyped) -> Integer?', + Integer, :try_convert, untyped + end end +end - def test_allbits? - 1.allbits?(1) - 2.allbits?(1) - 3.allbits?(ToInt.new) +class IntegerInstanceTest < Test::Unit::TestCase + include TestHelper + + testing 'Integer' + + def test_op_mod(method: :%) + # omit 'todo' end - def test_angle - 3.angle() + def test_op_and + # omit 'todo' end - def test_anybits? - 0xf0.anybits?(0xf) - 0xf1.anybits?(0xf) - 0xf1.anybits?(ToInt.new) + def test_op_mul + # omit 'todo' + end + + def test_op_pow + # omit 'todo' + end + + def test_op_add + # omit 'todo' + end + + def test_op_sub + # omit 'todo' + end + + def test_op_uneg + assert_send_type '() -> Integer', + 38, :-@ + end + + def test_op_div + # omit 'todo' + end + + def test_op_lt + # omit 'todo' + end + + def test_op_lsh + # omit 'todo' + end + + def test_op_leq + # omit 'todo' + end + + def test_op_cmp + # omit 'todo' + end + + def test_op_eq(method: :==) + with_untyped.and 38 do |other| + next unless defined? other.== # even for the `===` case heh + assert_send_type '(untyped) -> bool', + 38, method, other + end + end + + def test_op_eqq + test_op_eq(method: :===) + end + + def test_op_gt + # omit 'todo' + end + + def test_op_geq + # omit 'todo' + end + + def test_op_rsh + # omit 'todo' + end + + def test_op_aref + # omit 'todo' + end + + def test_op_xor + # omit 'todo' end - def test_arg - 3.arg + def test_abs(method: :magnitude) + assert_send_type '() -> Integer', + 38, method + assert_send_type '() -> Integer', + -38, method + end + + def test_allbits? + with_int 1 do |mask| + assert_send_type '(int) -> bool', + 0, :allbits?, mask + assert_send_type '(int) -> bool', + 1, :allbits?, mask + end + end + + def test_anybits? + with_int 1 do |mask| + assert_send_type '(int) -> bool', + 0, :anybits?, mask + assert_send_type '(int) -> bool', + 1, :anybits?, mask + end end def test_bit_length - 3.bit_length + assert_send_type '() -> Integer', + 38, :bit_length + assert_send_type '() -> Integer', + 3838, :bit_length end def test_ceil - 3.ceil - 3.ceil(10) - 3.ceil(ToInt.new) + assert_send_type '() -> Integer', + 38, :ceil + with_int(-1) do |digits| + assert_send_type '(int) -> Integer', + 38, :ceil, digits + end end def test_ceildiv - 3.ceildiv(10) - 3.ceildiv(1.3) + # omit 'todo' end def test_chr - 3.chr - 3.chr(Encoding::UTF_8) - 3.chr("UTF-7") - 3.chr(ToStr.new("ASCII-8BIT")) + assert_send_type '() -> String', + 38, :chr + + with_encoding do |encoding| + assert_send_type '(encoding) -> String', + 38, :chr, encoding + end end - def test_conj - 3.conj + def test_coerce + # omit 'todo' end def test_denominator - 3.denominator + assert_send_type '() -> 1', + 38, :denominator end def test_digits - 3.digits - 3.digits(3) - 3.digits(3.0) - 30.digits(ToInt.new) + assert_send_type '() -> Array[Integer]', + 38, :digits + + with_int 5 do |base| + assert_send_type '(int) -> Array[Integer]', + 38, :digits, base + end end def test_div - 30.div(10) + # omit 'todo' end - def test_div_mod - 3.divmod(3) - 40.divmod(1.0) - 30.divmod(30r) + def test_divmod + # omit 'todo' end - def test_down_to - 30.downto(1) {} - 30.downto(31) - 30.downto(4.2) + def test_downto + # omit 'todo' end def test_even? - 30.even? + assert_send_type '() -> bool', + 38, :even? + assert_send_type '() -> bool', + 39, :even? end def test_fdiv - 30.fdiv(30) - 30.fdiv(3r) - 30.fdiv(3.1) - end - - def test_finite? - 30.finite? + # omit 'todo' end def test_floor - 30.floor - 30.floor(3) - 30.floor(ToInt.new) + assert_send_type '() -> Integer', + 38, :floor + with_int(-1) do |digits| + assert_send_type '(int) -> Integer', + 38, :floor, digits + end end def test_gcd - 30.gcd(1) + assert_send_type '(Integer) -> Integer', + 38, :gcd, 2 + refute_send_type '(_ToInt) -> Integer', + 39, :gcd, ToInt.new(2) # explicitly only supports ints end def test_gcdlcm - 30.gcdlcm(31) + assert_send_type '(Integer) -> [Integer, Integer]', + 38, :gcdlcm, 2 + refute_send_type '(_ToInt) -> [Integer, Integer]', + 39, :gcdlcm, ToInt.new(2) # explicitly only supports ints + end + + def test_inspect + test_to_s(method: :inspect) + end + + def test_integer? + assert_send_type '() -> true', + 38, :integer? end def test_lcm - 30.lcm(50) + assert_send_type '(Integer) -> Integer', + 38, :lcm, 2 + refute_send_type '(_ToInt) -> Integer', + 39, :lcm, ToInt.new(2) # explicitly only supports ints end def test_magnitude - 30.magnitude + test_abs(method: :magnitude) end - def test_modulo_ - 30.modulo(30) - 30.modulo(3.1) - 30.modulo(3r/5) + def test_modulo + test_op_mod(method: :modulo) end def test_next - 30.next + test_succ(method: :next) end def test_nobits? - 0xf0.nobits?(0xf) - 0xf1.nobits?(0xf) - 30.nobits?(ToInt.new) + with_int 1 do |mask| + assert_send_type '(int) -> bool', + 0, :nobits?, mask + assert_send_type '(int) -> bool', + 1, :nobits?, mask + end end - def test_nonzero? - 30.nonzero? - 0.nonzero? + def test_numerator + assert_send_type '() -> 38', + 38, :numerator end - def test_numerator - 30.numerator + def test_odd? + assert_send_type '() -> bool', + 38, :odd? + assert_send_type '() -> bool', + 39, :odd? + end + + def test_ord + assert_send_type '() -> 38', + 38, :ord end def test_pow - 1.pow(30) - 1.pow(2.0) - 1.pow(30.to_c) - 3.pow(3, 5) + # omit 'todo' end - def test_quo - 3.quo(1) - 3.quo(2.1) - 3.quo(4r/5) - 3.quo(10.to_c) + def test_pred + assert_send_type '() -> Integer', + 1, :pred end def test_rationalize - 3.rationalize - 3.rationalize(30) + assert_send_type '() -> Rational', + 38, :rationalize + + with Numeric.new do |eps| + assert_send_type '(Numeric) -> Rational', + 38, :rationalize, eps + end end def test_remainder - 3.remainder(1) - 3.remainder(1.3) - 3.remainder(1r/3) + # omit 'todo' end def test_round - 13.round() - 13.round(half: :up) - 14.round(-1, half: :down) - 15.round(ToInt.new) + assert_send_type '() -> Integer', + 38, :round + + with_round_mode do |mode| + assert_send_type '(half: Numeric::round_mode) -> Integer', + 38, :round, half: mode + end + + with_int(-1) do |digits| + assert_send_type '(int) -> Integer', + 38, :round, digits + + with_round_mode do |mode| + assert_send_type '(int, half: Numeric::round_mode) -> Integer', + 38, :round, digits, half: mode + end + end end - def test_step - 3.step { break } - 3.step - 3.step(10, 2) {} - 3.step(10, 2) - 3.step(10, 1.1) {} - 3.step(10, 1.1) + def test_size + assert_send_type '() -> Integer', + 38, :size + assert_send_type '() -> Integer', + 3838, :size + end - 3.step(to: 30) { break } - 3.step(to: 30) - 3.step(to: 30, by: 100) {} - 3.step(to: 30, by: 100) - 3.step(to: 30, by: 10.0) {} - 3.step(to: 30, by: 10.0) + def test_succ(method: :succ) + assert_send_type '() -> Integer', + 1, method end def test_times - 3.times {} - 3.times + assert_send_type '() { (Integer) -> void } -> 38', + 38, :times do end + assert_send_type '() -> Enumerator[Integer, 38]', + 38, :times end - def test_truncate - 100.truncate - 100.truncate(10) - 100.truncate(ToInt.new(-2)) + def test_to_f + assert_send_type '() -> Float', + 38, :to_f end - def test_upto - 5.upto(10) {} - 5.upto(10.1) {} + def test_to_i(method: :to_i) + assert_send_type '() -> 38', + 38, method end -end + def test_to_int + test_to_i(method: :to_int) + end -class IntegerSingletonTest < Test::Unit::TestCase - include TestHelper + def test_to_r + assert_send_type '() -> Rational', + 38, :to_r + end - testing "singleton(::Integer)" + def test_to_s(method: :to_s) + assert_send_type '() -> String', + 38, method - def test_try_convert - assert_send_type( - "(Integer) -> Integer", - Integer, :try_convert, 10 - ) - assert_send_type( - "(ToInt) -> Integer", - Integer, :try_convert, ToInt.new(10) - ) - assert_send_type( - "(String) -> nil", - Integer, :try_convert, "10" - ) + with_int 8 do |base| + assert_send_type '(int) -> String', + 38, method, base + end end -end -class IntegerInstanceTest < Test::Unit::TestCase - include TestHelper + def test_truncate + assert_send_type '() -> Integer', + 38, :truncate + with_int(-1) do |digits| + assert_send_type '(int) -> Integer', + 38, :truncate, digits + end + end - testing "::Integer" + def test_upto + # omit 'todo' + end - def test_pow - assert_send_type "(Integer) -> Integer", - 1, :pow, 2 - assert_send_type "(Integer) -> Rational", - -2, :pow, -1 - assert_send_type "(Integer, Integer) -> Integer", - 1, :pow, 2, 10 - assert_send_type "(Float) -> Float", - 1, :pow, 1.0 - assert_send_type "(Float) -> Complex", - -9, :pow, 0.1 - assert_send_type "(Rational) -> Float", - 2, :pow, 1/2r - assert_send_type "(Rational) -> Rational", - 1, :pow, 1r - assert_send_type "(Rational) -> Complex", - -3, :pow, -4/3r - assert_send_type "(Complex) -> Complex", - 1, :pow, 1i + def test_zero? + assert_send_type '() -> bool', + 0, :zero? + assert_send_type '() -> bool', + 38, :zero? + end + + def test_op_or + # omit 'todo' + end + + def test_op_not + assert_send_type '() -> Integer', + 38, :~ end end diff --git a/test/stdlib/Module_test.rb b/test/stdlib/Module_test.rb index d4b285333f..25d1a1288f 100644 --- a/test/stdlib/Module_test.rb +++ b/test/stdlib/Module_test.rb @@ -1,4 +1,5 @@ require_relative "test_helper" +require_relative 'Module_test_helper' class ModuleSingletonTest < Test::Unit::TestCase include TestHelper @@ -8,13 +9,32 @@ class ModuleSingletonTest < Test::Unit::TestCase def test_used_modules assert_send_type "() -> Array[Module]", Module, :used_modules + + assert_type 'Array[Module]', + ModuleTestHelperRefinement::USED_MODULES end def test_used_refinements - assert_send_type( - "() -> Array[Refinement]", - Module, :used_refinements - ) + assert_send_type "() -> Array[Refinement]", + Module, :used_refinements + + assert_type 'Array[Refinement]', + ModuleTestHelperRefinement::USED_REFINEMENTS + end + + def test_constants + assert_send_type '() -> Array[Symbol]', + Module, :constants + + with_boolish do |inherit| + assert_send_type '(boolish) -> Array[Symbol]', + Module, :constants, inherit + end + end + + def test_nesting + assert_send_type '() -> Array[Module]', + Module, :nesting end end @@ -30,65 +50,158 @@ def foo(meth, *args, &block) end end + module RefinedModule + refine String do + end + end + + def with_untyped_singleton_possible + with_untyped do |untyped| + next if Integer === untyped || Float === untyped || Symbol === untyped + untyped = ::Kernel.instance_method(:dup).bind_call(untyped) if ::Kernel.instance_method(:frozen?).bind_call(untyped) + yield untyped + end + end + + def test_op_lt + with Object, Float, Hash do |mod| + assert_send_type '(Module) -> bool?', + Numeric, :<, mod + end + end + + def test_op_le + with Object, Float, Hash do |mod| + assert_send_type '(Module) -> bool?', + Numeric, :<=, mod + end + end + + def test_op_gt + with Object, Float, Hash do |mod| + assert_send_type '(Module) -> bool?', + Numeric, :>, mod + end + end + + def test_op_ge + with Object, Float, Hash do |mod| + assert_send_type '(Module) -> bool?', + Numeric, :>=, mod + end + end + + def test_op_cmp + with Object, Float, Numeric, Hash do |mod| + assert_send_type '(untyped) -> (-1 | 0 | 1)?', + Numeric, :<=>, mod + end + + with_untyped do |untyped| + next if Module === untyped + assert_send_type '(untyped) -> nil', + Numeric, :<=>, untyped + end + end + + def test_op_eq + with_untyped.and Numeric, Module do |untyped| + assert_send_type '(untyped) -> bool', + Numeric, :==, untyped + end + end + + def test_op_eqq + with_untyped.and Numeric, Module do |untyped| + assert_send_type '(untyped) -> bool', + Numeric, :===, untyped + end + end + def test_include assert_send_type "(Module) -> Module", Module.new, :include, Module.new - assert_send_type "(Module, Module) -> Module", + assert_send_type "(Module, *Module) -> Module", Module.new, :include, Module.new, Module.new end def test_prepend assert_send_type "(Module) -> Module", Module.new, :prepend, Module.new - assert_send_type "(Module, Module) -> Module", + assert_send_type "(Module, *Module) -> Module", Module.new, :prepend, Module.new, Module.new end def test_refine + assert_visibility :private, :refine + assert_send_type "(Module) { () -> void } -> Refinement", - Foo, :refine, String do nil end + RefinedModule, :refine, Integer do nil end end def test_refinements - assert_send_type( - "() -> Array[Refinement]", - Foo, :refinements - ) + assert_send_type "() -> Array[Refinement]", + Module.new, :refinements + + assert_send_type "() -> Array[Refinement]", + RefinedModule, :refinements end def test_const_source_location - assert_send_type "(Symbol) -> [String, Integer]", - Foo, :const_source_location, :BAR - assert_send_type "(Symbol) -> nil", - Foo, :const_source_location, :UNKNOWN - assert_send_type "(String) -> [String, Integer]", - Foo, :const_source_location, "BAR" - assert_send_type "(String) -> nil", - Foo, :const_source_location, "UNKNOWN" - assert_send_type "(Symbol, true) -> [String, Integer]", - Foo, :const_source_location, :BAR, true - assert_send_type "(String, nil) -> [String, Integer]", - Foo, :const_source_location, "BAR", nil - assert_send_type "(Symbol) -> [ ]", - Foo, :const_source_location, :String - assert_send_type "(String) -> [ ]", - Foo, :const_source_location, "String" - assert_send_type "(Symbol, true) -> [ ]", - Foo, :const_source_location, :String, true - assert_send_type "(String, nil) -> nil", - Foo, :const_source_location, "String", nil - end - - def test_module_eval - assert_send_type "(String) -> nil", - Foo, :module_eval, 'nil' - assert_send_type "(String, String) -> nil", - Foo, :module_eval, 'nil', __FILE__ - assert_send_type "(String, String, Integer) -> nil", - Foo, :module_eval, 'nil', __FILE__, 42 - - assert_send_type "() { (Module) -> nil } -> nil", - Foo, :module_eval do nil end + with_interned :BAR do |interned| + assert_send_type "(interned) -> [String, Integer]", + Foo, :const_source_location, interned + + with_boolish.and_nil do |boolish| + assert_send_type "(interned, boolish) -> [String, Integer]", + Foo, :const_source_location, interned, boolish + end + end + + with_interned :UNKNOWN do |interned| + assert_send_type "(interned) -> nil", + Foo, :const_source_location, interned + + with_boolish.and_nil do |boolish| + assert_send_type "(interned, boolish) -> nil", + Foo, :const_source_location, interned, boolish + end + end + + with_interned :String do |interned| + assert_send_type "(interned) -> []", + Foo, :const_source_location, interned + + with_boolish.and_nil do |boolish| + assert_send_type "(interned, boolish) -> []?", + Foo, :const_source_location, interned, boolish + end + end + end + + def test_module_eval(method: :module_eval) + with_string 'nil' do |code| + assert_send_type "(string) -> untyped", + Foo, method, code + + with_string.and_nil do |file| + assert_send_type "(string, string?) -> untyped", + Foo, method, code, file + + with_int do |lineno| + assert_send_type "(string, string?, int) -> untyped", + Foo, method, code, file, lineno + end + end + end + + assert_send_type '() { (Module) [self: Foo] -> Rational } -> Rational', + Foo, method do 1r end + end + + def test_module_exec(method: :module_exec) + assert_send_type '(*String) { (*String) [self: Foo] -> Integer } -> Integer', + Foo, method, '1', '2' do |*x| x.join.to_i end end def test_module_function @@ -98,44 +211,51 @@ def foo; end def bar; end end - assert_send_type( - "() -> nil", - mod, :module_function - ) - assert_send_type( - "(Symbol) -> Symbol", - mod, :module_function, :foo - ) - assert_send_type( - "(String) -> String", - mod, :module_function, "foo" - ) - assert_send_type( - "(ToStr) -> ToStr", - mod, :module_function, ToStr.new("foo") - ) - - assert_send_type( - "(Symbol, String) -> Array[Symbol | String]", - mod, :module_function, :foo, "bar" - ) + assert_visibility :private, :module_function + + # No arguments + assert_send_type '() -> nil', + mod, :module_function + + # Single argument + assert_send_type '(Symbol) -> Symbol', + mod, :module_function, :foo + + with_string 'foo' do |foo| + assert_send_type '[T < _ToStr] (T) -> T', + mod, :module_function, foo + end + + with_interned 'foo' do |foo| + assert_send_type '(interned) -> interned', + mod, :module_function, foo + end + + # Multiple arguments + assert_send_type '(Symbol, *Symbol) -> Array[Symbol]', + mod, :module_function, :foo, :bar + + with_string 'foo' do |foo| + with_string 'bar' do |bar| + assert_send_type '[T < _ToStr] (T, *T) -> Array[T]', + mod, :module_function, foo, bar + end + end + + with_interned 'foo' do |foo| + with_interned 'bar' do |bar| + assert_send_type '(interned, *interned) -> Array[interned]', + mod, :module_function, foo, bar + end + end end def test_class_eval - assert_send_type "(String) -> nil", - Foo, :class_eval, 'nil' - assert_send_type "(String, String) -> nil", - Foo, :class_eval, 'nil', __FILE__ - assert_send_type "(String, String, Integer) -> nil", - Foo, :class_eval, 'nil', __FILE__, 42 - - assert_send_type "() { (Module) -> nil } -> nil", - Foo, :class_eval do nil end + test_module_eval(method: :class_eval) end def test_class_exec - assert_send_type '(*String) { (*String) [self: Foo] -> Integer } -> Integer', - Foo, :class_exec, '1', '2' do |*x| x.join.to_i end + test_module_exec(method: :class_exec) end def test_alias_method @@ -144,297 +264,1015 @@ def foo end end - omit_if(mod.alias_method(:bar, :foo).equal?(mod)) - assert_send_type '(::Symbol new_name, ::Symbol old_name) -> ::Symbol', - mod, :alias_method, :bar2, :foo - assert_send_type '(::String new_name, ::String old_name) -> ::Symbol', - mod, :alias_method, 'bar3', 'foo' + with_interned :bar do |new_name| + with_interned :foo do |old_name| + assert_send_type '(interned, interned) -> Symbol', + mod, :alias_method, new_name, old_name + end + end end - def test_private + # Helper for `private`, `public`, and `protected`, as they all function identically + def classlevel_visibility_helper(visibility) mod = Module.new do def foo; end - def bar; end end - assert_send_type( - "() -> nil", - mod, :private - ) - assert_send_type( - "(Symbol) -> Symbol", - mod, :private, :foo - ) - assert_send_type( - "(String) -> String", - mod, :private, "foo" - ) - assert_send_type( - "(ToStr) -> ToStr", - mod, :private, ToStr.new("foo") - ) - - assert_send_type( - "(Symbol, String) -> Array[interned]", - mod, :private, :foo, "bar" - ) - - assert_send_type( - "(Array[Symbol | String]) -> Array[Symbol | String]", - mod, :private, [:foo, "bar"] - ) + assert_visibility :private, visibility + + # No arguments + assert_send_type '() -> nil', + mod, visibility + + # Single argument + assert_send_type '(Symbol) -> Symbol', + mod, visibility, :foo + + with_string 'foo' do |foo| + assert_send_type '[T < _ToStr] (T) -> T', + mod, visibility, foo + end + + with_interned 'foo' do |foo| + assert_send_type '(interned) -> interned', + mod, visibility, foo + end + + # Multiple arguments + assert_send_type '(Symbol, *Symbol) -> Array[Symbol]', + mod, visibility, :foo, :bar + + with_string 'foo' do |foo| + with_string 'bar' do |bar| + assert_send_type '[T < _ToStr] (T, *T) -> Array[T]', + mod, visibility, foo, bar + end + end + + with_interned 'foo' do |foo| + with_interned 'bar' do |bar| + assert_send_type '(interned, *interned) -> Array[interned]', + mod, visibility, foo, bar + end + end + + # With the array version + assert_send_type '(Array[Symbol]) -> Array[Symbol]', + mod, visibility, [:foo] + with_string 'foo' do |foo| + assert_send_type '[T < _ToStr] (Array[T]) -> Array[T]', + mod, visibility, [foo] + end + with_interned 'foo' do |foo| + with_interned 'bar' do |bar| + assert_send_type '(Array[interned]) -> Array[interned]', + mod, visibility, [foo, bar] + end + end end + + def test_private + classlevel_visibility_helper(:private) + end + + def test_protected + classlevel_visibility_helper(:protected) + end + + def test_public + classlevel_visibility_helper(:public) + end + + def test_private_class_method mod = Module.new do def self.foo; end def self.bar; end end - assert_send_type( - "(Symbol) -> Module", - mod, :private_class_method, :foo - ) - - assert_send_type( - "(Symbol, Symbol) -> Module", - mod, :private_class_method, :foo, :bar - ) + disable_verbose do + assert_send_type '() -> Module', + mod, :private_class_method + end - assert_send_type( - "(String) -> Module", - mod, :private_class_method, "foo" - ) + with_interned :foo do |foo| + assert_send_type '(interned) -> Module', + mod, :private_class_method, foo + assert_send_type '(Array[interned]) -> Module', + mod, :private_class_method, [foo] + with_interned :bar do |bar| + assert_send_type '(interned, *interned) -> Module', + mod, :private_class_method, foo, bar + assert_send_type '(Array[interned]) -> Module', + mod, :private_class_method, [foo, bar] + end + end + end + def test_public_class_method + mod = Module.new do + def self.foo; end + def self.bar; end + end - assert_send_type( - "(String, String) -> Module", - mod, :private_class_method, "foo", "bar" - ) + disable_verbose do + assert_send_type '() -> Module', + mod, :public_class_method + end - assert_send_type( - "(Array[Symbol | String]) -> Module", - mod, :private_class_method, [:foo, "bar"] - ) + with_interned :foo do |foo| + assert_send_type '(interned) -> Module', + mod, :public_class_method, foo + assert_send_type '(Array[interned]) -> Module', + mod, :public_class_method, [foo] + with_interned :bar do |bar| + assert_send_type '(interned, *interned) -> Module', + mod, :public_class_method, foo, bar + assert_send_type '(Array[interned]) -> Module', + mod, :public_class_method, [foo, bar] + end + end end - def test_protected - mod = Module.new do - def foo; end + def test_attr + assert_send_type '() -> Array[Symbol]', + Module.new, :attr + + with_interned :foo do |foo| + with_bool do |writer| + # attr with bool arguments yields a warning normally + disable_verbose do + assert_send_type '(interned, bool) -> Array[Symbol]', + Module.new, :attr, foo, writer + end + end - def bar; end + assert_send_type '(interned) -> Array[Symbol]', + Module.new, :attr, foo + + with_interned :bar do |bar| + assert_send_type '(*interned) -> Array[Symbol]', + Module.new, :attr, foo, bar + end end + end + + def test_attr_reader + assert_send_type '() -> Array[Symbol]', + Module.new, :attr_reader + + with_interned :foo do |foo| + assert_send_type '(interned) -> Array[Symbol]', + Module.new, :attr_reader, foo - assert_send_type( - "() -> nil", - mod, :protected - ) - assert_send_type( - "(Symbol) -> Symbol", - mod, :protected, :foo - ) - assert_send_type( - "(String) -> String", - mod, :protected, "foo" - ) - assert_send_type( - "(ToStr) -> ToStr", - mod, :protected, ToStr.new("foo") - ) - - assert_send_type( - "(Symbol, String) -> Array[Symbol | String]", - mod, :protected, :foo, "bar" - ) - - assert_send_type( - "(Array[Symbol | String]) -> Array[Symbol | String]", - mod, :protected, [:foo, "bar"] - ) + with_interned :bar do |bar| + assert_send_type '(interned, interned) -> Array[Symbol]', + Module.new, :attr_reader, foo, bar + end + end end - def test_public - mod = Module.new do - def foo; end + def test_attr_writer + assert_send_type '() -> Array[Symbol]', + Module.new, :attr_writer - def bar; end + with_interned :foo do |foo| + assert_send_type '(interned) -> Array[Symbol]', + Module.new, :attr_writer, foo + + with_interned :bar do |bar| + assert_send_type '(interned, interned) -> Array[Symbol]', + Module.new, :attr_writer, foo, bar + end end + end - assert_send_type( - "() -> nil", - mod, :public - ) - assert_send_type( - "(Symbol) -> Symbol", - mod, :public, :foo - ) - assert_send_type( - "(String) -> String", - mod, :public, "foo" - ) - assert_send_type( - "(ToStr) -> ToStr", - mod, :public, ToStr.new("foo") - ) - - assert_send_type( - "(Symbol, String) -> Array[interned]", - mod, :public, :foo, "bar" - ) - - assert_send_type( - "(Array[Symbol | String]) -> Array[Symbol | String]", - mod, :public, [:foo, "bar"] - ) + def test_attr_accessor + assert_send_type '() -> Array[Symbol]', + Module.new, :attr_accessor + + with_interned :foo do |foo| + assert_send_type '(interned) -> Array[Symbol]', + Module.new, :attr_accessor, foo + + with_interned :bar do |bar| + assert_send_type '(interned, interned) -> Array[Symbol]', + Module.new, :attr_accessor, foo, bar + end + end end - def test_public_class_method + def test_ruby2_keywords mod = Module.new do - def self.foo; end - def self.bar; end + def foo(*x) = 3 + def bar(*x) = 3 end - assert_send_type( - "(Symbol) -> Module", - mod, :public_class_method, :foo - ) + assert_visibility :private, :ruby2_keywords - assert_send_type( - "(Symbol, Symbol) -> Module", - mod, :public_class_method, :foo, :bar - ) + with_interned :foo do |foo| + assert_send_type '(interned) -> nil', + mod, :ruby2_keywords, foo + with_interned :bar do |bar| + assert_send_type '(interned, *interned) -> nil', + mod, :ruby2_keywords, foo, bar + end + end + end - assert_send_type( - "(String) -> Module", - mod, :public_class_method, "foo" - ) + def test_set_temporary_name + assert_send_type '(nil) -> Module', + Module.new, :set_temporary_name, nil - assert_send_type( - "(String, String) -> Module", - mod, :public_class_method, "foo", "bar" - ) + with_string "fake_name" do |name| + assert_send_type '(string) -> Module', + Module.new, :set_temporary_name, name + end + end - assert_send_type( - "(Array[Symbol | String]) -> Module", - mod, :public_class_method, [:foo, "bar"] - ) + def test_to_s(method: :to_s) + assert_send_type '() -> String', + Module.new, method end - def test_attr + def test_inspect + test_to_s(method: :inspect) + end + + def test_ancestors + assert_send_type '() -> Array[Module]', + Module.new, :ancestors + assert_send_type '() -> Array[Module]', + Module, :ancestors + end + + def test_autoload + with_interned :Constant do |constant| + with_path do |path| + assert_send_type '(interned, path) -> nil', + Module.new, :autoload, constant, path + end + end + end + + def test_autoload? + autoloaded = Module.new + autoloaded.autoload(:Constant, 'Bar') + + with_interned :Constant do |constant| + assert_send_type '(interned) -> nil', + Module.new, :autoload?, constant + assert_send_type '(interned) -> String', + autoloaded, :autoload?, constant + assert_send_type '(interned) -> String', + Module.new.include(autoloaded), :autoload?, constant + + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> nil', + Module.new, :autoload?, constant, inherit + assert_send_type '(interned, boolish) -> String', + autoloaded, :autoload?, constant, inherit + assert_send_type '(interned, boolish) -> String?', + Module.new.include(autoloaded), :autoload?, constant, inherit + end + end + end + + def test_class_variable_defined? mod = Module.new - assert_send_type( - "(*interned arg0) -> Array[Symbol]", - mod, :attr, :foo - ) + + with_interned :@@module_test_class_variable_defined do |cvar| + assert_send_type "(interned) -> bool", + mod, :class_variable_defined?, cvar + end + + mod.class_variable_set :@@module_test_class_variable_defined, 12 + with_interned :@@module_test_class_variable_defined do |cvar| + assert_send_type "(interned) -> bool", + mod, :class_variable_defined?, cvar + end end - def test_attr_reader + def test_class_variable_get + mod = Module.new + + # If it's not set it raises an exception + mod.class_variable_set :@@module_test_class_variable_get, 12 + + with_interned :@@module_test_class_variable_get do |cvar| + assert_send_type "(interned) -> untyped", + mod, :class_variable_get, cvar + end + end + + def test_class_variable_set mod = Module.new - assert_send_type( - "(Symbol) -> Array[Symbol]", - mod, :attr_reader, :foo - ) + with_interned :@@module_test_class_variable_set do |cvar| + with_untyped do |value| + assert_send_type "[T] (interned, T) -> T", + mod, :class_variable_set, cvar, value + end + end + end - assert_send_type( - "(Symbol, Symbol) -> Array[Symbol]", - mod, :attr_reader, :foo, :bar - ) + def test_class_variables + mod = Module.new do + include Module.new { + class_variable_set :@@module_test_class_variables_1, 12 + } - assert_send_type( - "(String) -> Array[Symbol]", - mod, :attr_reader, "foo" - ) + class_variable_set :@@module_test_class_variables_2, 34 + end - assert_send_type( - "(String, String) -> Array[Symbol]", - mod, :attr_reader, "foo", "bar" - ) + assert_send_type '() -> Array[Symbol]', + Module.new, :class_variables + assert_send_type '() -> Array[Symbol]', + mod, :class_variables - assert_send_type( - "(Symbol, String) -> Array[Symbol]", - mod, :attr_reader, :foo, "bar" - ) + with_boolish do |inherit| + assert_send_type '(boolish) -> Array[Symbol]', + Module.new, :class_variables, inherit + assert_send_type '(boolish) -> Array[Symbol]', + mod, :class_variables, inherit + end end - def test_attr_writer + def test_const_defined? mod = Module.new - assert_send_type( - "(Symbol) -> Array[Symbol]", - mod, :attr_writer, :foo - ) + # Before defining the constant + with_interned :ModuleTestConstDefined do |name| + assert_send_type '(interned) -> bool', + mod, :const_defined?, name - assert_send_type( - "(Symbol, Symbol) -> Array[Symbol]", - mod, :attr_writer, :foo, :bar - ) + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :const_defined?, name, inherit + end + end - assert_send_type( - "(String) -> Array[Symbol]", - mod, :attr_writer, "foo" - ) + mod.const_set :ModuleTestConstDefined, 123 - assert_send_type( - "(String, String) -> Array[Symbol]", - mod, :attr_writer, "foo", "bar" - ) + # After defining the constant + with_interned :ModuleTestConstDefined do |name| + assert_send_type '(interned) -> bool', + mod, :const_defined?, name - assert_send_type( - "(Symbol, String) -> Array[Symbol]", - mod, :attr_writer, :foo, "bar" - ) + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :const_defined?, name, inherit + end + end end - def test_attr_accessor + def test_const_get mod = Module.new - assert_send_type( - "(Symbol) -> Array[Symbol]", - mod, :attr_accessor, :foo - ) + # Define the constant first to avoid NameError paths + mod.const_set :ModuleTestConstGet, 123 - assert_send_type( - "(Symbol, Symbol) -> Array[Symbol]", - mod, :attr_accessor, :foo, :bar - ) + with_interned :ModuleTestConstGet do |name| + assert_send_type '(interned) -> untyped', + mod, :const_get, name - assert_send_type( - "(String) -> Array[Symbol]", - mod, :attr_accessor, "foo" - ) + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> untyped', + mod, :const_get, name, inherit + end + end + end + + def test_const_missing + assert_type_meth = method(:assert_type) + mod = Module.new do + define_singleton_method :const_missing do |name| + assert_type_meth.call('Symbol', name) + 1r + end + end - assert_send_type( - "(String, String) -> Array[Symbol]", - mod, :attr_accessor, "foo", "bar" - ) + assert_type 'Rational', + mod::Foo # Test the `::` syntax works - assert_send_type( - "(Symbol, String) -> Array[Symbol]", - mod, :attr_accessor, :foo, "bar" - ) + # Make sure that `const_get` also always passes a symbol + with_interned :Foo do |name| + assert_send_type '(interned) -> Rational', + mod, :const_get, name + end end - def test_ruby2_keywords - assert_send_type( - "(Symbol) -> nil", - Foo, :ruby2_keywords, :foo - ) + def test_const_set + with_interned :ModuleTestConstSet do |name| + with_untyped do |value| + assert_send_type '[T] (interned, T) -> T', + Module.new, :const_set, name, value + end + end end - def test_set_temporary_name - mod = Module.new + def test_define_method + with_interned :meth do |name| + assert_send_type '(interned, ^(?) [self: top] -> untyped) -> Symbol', + Module.new, :define_method, name, proc {} - with_string "fake_name" do |name| - assert_send_type( - "(::string) -> ::Module", - mod, :set_temporary_name, name - ) + assert_send_type '(interned, Method) -> Symbol', + Module.new, :define_method, name, method(:puts) + + assert_send_type '(interned, UnboundMethod) -> Symbol', + Module.new, :define_method, name, method(:puts).unbind + + assert_send_type '(interned) { (?) [self: top] -> untyped } -> Symbol', + Module.new, :define_method, name do end end + end + + def constant_visibility_helper(method) + mod = Module.new do + const_set :Foo, 3 + const_set :Bar, 4 + end + + # No arguments yields a warning with `-w` + disable_verbose do + assert_send_type '() -> Module', + mod, method + end + + with_interned :Foo do |foo| + assert_send_type '(interned) -> Module', + mod, method, foo + + with_interned :Bar do |bar| + assert_send_type '(interned, *interned) -> Module', + mod, method, foo, bar + end + end + end + + def test_deprecate_constant + constant_visibility_helper(:deprecate_constant) + end + + def test_freeze + assert_send_type '() -> Module', + Module.new, :freeze + end + + def test_included_modules + assert_send_type '() -> Array[Module]', + Module.new, :included_modules + assert_send_type '() -> Array[Module]', + Module.new{ include Module.new }, :included_modules + end + + def test_initialize + assert_send_type '() -> void', + Module.new, :initialize + + assert_send_type '() { (Module) [self: Module] -> void } -> void', + Module.new, :initialize do end + end + + def test_initialize_clone + assert_send_type '(Module) -> void', + Module.new, :initialize_clone, Module.new + + + with_bool.and_nil do |freeze| + assert_send_type '(Module, freeze: bool?) -> void', + Module.new, :initialize_clone, Module.new, freeze: freeze + end + end + + def test_instance_method + mod = Module.new do + def foo = 3 + private def bar = 4 + end + + with_interned :foo do |name| + assert_send_type '(interned) -> UnboundMethod', + mod, :instance_method, name + end + + with_interned :bar do |name| + assert_send_type '(interned) -> UnboundMethod', + mod, :instance_method, name + end + end + + def test_instance_methods + mod = Module.new do + include Module.new { + public def foo1 = 1 + private def foo2 = 1 + protected def foo3 = 1 + } + public def bar1 = 1 + private def bar2 = 1 + protected def bar3 = 1 + end + + assert_send_type '() -> Array[Symbol]', + Module.new, :instance_methods + assert_send_type '() -> Array[Symbol]', + mod, :instance_methods + + with_boolish do |include_super| + assert_send_type '(boolish) -> Array[Symbol]', + mod, :instance_methods, include_super + end + end + + def test_method_defined? + mod = Module.new do + include Module.new { def bar = 1 } + def foo = 2 + end + + with_interned :foo do |foo| + assert_send_type '(interned) -> bool', + mod, :method_defined?, foo + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :method_defined?, foo, inherit + end + end + + with_interned :bar do |bar| + assert_send_type '(interned) -> bool', + mod, :method_defined?, bar + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :method_defined?, bar, inherit + end + end + + with_interned :baz do |baz| + assert_send_type '(interned) -> bool', + mod, :method_defined?, baz + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :method_defined?, baz, inherit + end + end + end + + def test_name + assert_send_type '() -> nil', + Module.new, :name + + assert_send_type '() -> String', + Comparable, :name + end + + def test_private_constant + constant_visibility_helper(:private_constant) + end + + def test_private_instance_methods + mod = Module.new do + include Module.new { + private def bar = 4 + } + + private def foo = 3 + end + + assert_send_type '() -> Array[Symbol]', + Module.new, :private_instance_methods + assert_send_type '() -> Array[Symbol]', + mod, :private_instance_methods + + with_boolish do |include_super| + assert_send_type '(boolish) -> Array[Symbol]', + mod, :private_instance_methods, include_super + end + end + + def test_private_method_defined? + mod = Module.new do + include Module.new { private def bar = 1 } + private def foo = 2 + public def baz = 3 + end + + with_interned :foo do |foo| + assert_send_type '(interned) -> bool', + mod, :private_method_defined?, foo + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :private_method_defined?, foo, inherit + end + end + + with_interned :bar do |bar| + assert_send_type '(interned) -> bool', + mod, :private_method_defined?, bar + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :private_method_defined?, bar, inherit + end + end + + with_interned :baz do |baz| + assert_send_type '(interned) -> bool', + mod, :private_method_defined?, baz + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :private_method_defined?, baz, inherit + end + end + + with_interned :quux do |quux| + assert_send_type '(interned) -> bool', + mod, :private_method_defined?, quux + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :private_method_defined?, quux, inherit + end + end + end + + def test_protected_instance_methods + mod = Module.new do + include Module.new { + protected def bar = 4 + } + + protected def foo = 3 + end + + assert_send_type '() -> Array[Symbol]', + Module.new, :protected_instance_methods + assert_send_type '() -> Array[Symbol]', + mod, :protected_instance_methods + + with_boolish do |include_super| + assert_send_type '(boolish) -> Array[Symbol]', + mod, :protected_instance_methods, include_super + end + end + + def test_protected_method_defined? + mod = Module.new do + include Module.new { protected def bar = 1 } + protected def foo = 2 + public def baz = 3 + end + + with_interned :foo do |foo| + assert_send_type '(interned) -> bool', + mod, :protected_method_defined?, foo + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :protected_method_defined?, foo, inherit + end + end + + with_interned :bar do |bar| + assert_send_type '(interned) -> bool', + mod, :protected_method_defined?, bar + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :protected_method_defined?, bar, inherit + end + end + + with_interned :baz do |baz| + assert_send_type '(interned) -> bool', + mod, :protected_method_defined?, baz + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :protected_method_defined?, baz, inherit + end + end + + with_interned :quux do |quux| + assert_send_type '(interned) -> bool', + mod, :protected_method_defined?, quux + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :protected_method_defined?, quux, inherit + end + end + end + + def test_public_constant + constant_visibility_helper(:public_constant) + end + + def test_public_instance_method + mod = Module.new do + def foo = 3 + end + + with_interned :foo do |name| + assert_send_type '(interned) -> UnboundMethod', + mod, :instance_method, name + end + end + + def test_public_instance_methods + mod = Module.new do + include Module.new { + def bar = 4 + } + + def foo = 3 + end + + assert_send_type '() -> Array[Symbol]', + Module.new, :public_instance_methods + assert_send_type '() -> Array[Symbol]', + mod, :public_instance_methods + + with_boolish do |include_super| + assert_send_type '(boolish) -> Array[Symbol]', + mod, :public_instance_methods, include_super + end + end + + def test_public_method_defined? + mod = Module.new do + include Module.new { public def bar = 1 } + public def foo = 2 + private def baz = 3 + end + + with_interned :foo do |foo| + assert_send_type '(interned) -> bool', + mod, :public_method_defined?, foo + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :public_method_defined?, foo, inherit + end + end + + with_interned :bar do |bar| + assert_send_type '(interned) -> bool', + mod, :public_method_defined?, bar + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :public_method_defined?, bar, inherit + end + end + + with_interned :baz do |baz| + assert_send_type '(interned) -> bool', + mod, :public_method_defined?, baz + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :public_method_defined?, baz, inherit + end + end + + with_interned :quux do |quux| + assert_send_type '(interned) -> bool', + mod, :public_method_defined?, quux + with_boolish do |inherit| + assert_send_type '(interned, boolish) -> bool', + mod, :public_method_defined?, quux, inherit + end + end + end + + def test_remove_class_variable + with_interned :@@module_test_remove_class_variable do |cvar| + mod = Module.new do + class_variable_set(:@@module_test_remove_class_variable, 123) + end + + assert_send_type '(interned) -> untyped', + mod, :remove_class_variable, cvar + end + end + + def test_remove_method + assert_send_type '() -> Module', + Module.new, :remove_method + + with_interned :foo do |foo| + assert_send_type '(interned) -> Module', + Module.new { def foo = 3 }, :remove_method, foo + with_interned :bar do |bar| + assert_send_type '(*interned) -> Module', + Module.new { def foo = 3; def bar = 4 }, :remove_method, foo, bar + end + end + end + + def test_singleton_class? + assert_send_type '() -> bool', + Module, :singleton_class? + assert_send_type '() -> bool', + Module.singleton_class, :singleton_class? + end + + def test_undef_method + assert_send_type '() -> Module', + Module.new, :undef_method + + with_interned :foo do |foo| + assert_send_type '(interned) -> Module', + Module.new { def foo = 3 }, :undef_method, foo + with_interned :bar do |bar| + assert_send_type '(*interned) -> Module', + Module.new { def foo = 3; def bar = 4 }, :undef_method, foo, bar + end + end + end + + def test_undefined_instance_methods + mod = Module.new do + def foo = 4 + undef foo + end + + assert_send_type '() -> Array[Symbol]', + Module.new, :undefined_instance_methods + assert_send_type '() -> Array[Symbol]', + mod, :undefined_instance_methods + end + + def test_append_features + assert_visibility :private, :append_features + + assert_send_type '(Module) -> Module', + Module.new, :append_features, Module.new + end + + def test_const_added + assert_visibility :private, :const_added + + # const_added directly works + assert_send_type '(Symbol) -> void', + Module.new, :const_added, :foo + + # Setting the constant also works + const_added_module = proc do + assert_type_meth = method(:assert_type) + mod = Module.new do + define_singleton_method :const_added do |name| + assert_type_meth.call('Symbol', name) + end + end + end + + # Make sure the `::` assignment passes a symbol + eval <<~EOS + const_added_module.call()::Foo = 3 + EOS + + # Make sure that `const_set` also always passes a symbol to the `const_added` + with_interned :Foo do |name| + const_added_module.call().const_set(name, 2r) + end + end + + def test_extend_object + assert_visibility :private, :extend_object + + with_untyped_singleton_possible do |untyped| + assert_send_type '[T] (T) -> T', + Module.new, :extend_object, untyped + end + + # No need to make sure `object.extend(module)` works because the signature + # is `(T) -> T`, which means it can take any type (and we aren't testing the + # return value of `extend`) + end + + def test_extended + assert_visibility :private, :extended + + with_untyped_singleton_possible do |untyped| + assert_send_type '(untyped) -> void', + Module.new, :extended, untyped + end + + # No need to make sure `object.extend(module)` works because the signature + # is `(untyped) -> void`, which means it can take any type, and we dont care + # about the return value. + end + + def test_included + assert_visibility :private, :included + + assert_send_type '(Module) -> void', + Module.new, :included, Module.new + + assert_send_type '(Module) -> void', + Module.new, :included, Class.new + end + + def test_method_added + assert_visibility :private, :method_added + + + # method_added directly works + assert_send_type '(Symbol) -> void', + Module.new, :method_added, :foo + + # make sure using `with_intern` always passes a symbol + assert_type_meth = method(:assert_type) + mod = Module.new do + define_singleton_method :method_added do |name| + assert_type_meth.call('Symbol', name) + end + end + + with_interned :foo do |name| + mod.define_method(:foo) {} + mod.undef_method(:foo) # avoid warnings + end + end + + def test_method_removed + assert_visibility :private, :method_removed + + # method_removed directly works + assert_send_type '(Symbol) -> void', + Module.new, :method_removed, :foo + + # make sure using `with_intern` always passes a symbol + assert_type_meth = method(:assert_type) + mod = Module.new do + define_singleton_method :method_removed do |name| + assert_type_meth.call('Symbol', name) + end + end + + with_interned :foo do |name| + mod.define_method(:foo) {} + mod.remove_method(name) + end + end + + def test_method_undefined + assert_visibility :private, :method_undefined + + # method_undefined directly works + assert_send_type '(Symbol) -> void', + Module.new, :method_undefined, :foo + + # make sure using `with_intern` always passes a symbol + assert_type_meth = method(:assert_type) + mod = Module.new do + define_singleton_method :method_undefined do |name| + assert_type_meth.call('Symbol', name) + end + end + + with_interned :foo do |name| + mod.define_method(:foo) {} + mod.undef_method(name) + end + end + + def test_prepend_features + assert_visibility :private, :prepend_features + + assert_send_type '(Module) -> Module', + Module.new, :prepend_features, Module.new + + assert_send_type '(Module) -> Module', + Module.new, :prepend_features, Class.new + end + + def test_prepended + assert_visibility :private, :prepended + + assert_send_type '(Module) -> void', + Module.new, :prepended, Module.new + + assert_send_type '(Module) -> void', + Module.new, :prepended, Class.new + end + + def test_remove_const + assert_visibility :private, :remove_const + + with_interned :Foo do |name| + mod = Module.new + mod.const_set :Foo, 1r + + assert_send_type '(interned) -> untyped', + mod, :remove_const, name + end + end + + + module UsingModule + UsingReturnValue = using Module.new + end + + def test_using + assert_visibility :private, :using - assert_send_type( - "(nil) -> Module", - mod, :set_temporary_name, nil - ) + # Cant actually test `using` in modules, so this is the best we got + assert_type 'Module', UsingModule::UsingReturnValue end end diff --git a/test/stdlib/Module_test_helper.rb b/test/stdlib/Module_test_helper.rb new file mode 100644 index 0000000000..113481a76d --- /dev/null +++ b/test/stdlib/Module_test_helper.rb @@ -0,0 +1,12 @@ +# Because refinements are scoped per-file, we have to make a separate file to +# query refinements, so they don't mess up the rest of the `Module_test`. +module ModuleTestHelperRefinement + refine Class.new do + def blah = 34 + end +end + +using ModuleTestHelperRefinement + +ModuleTestHelperRefinement::USED_MODULES = Module.used_modules +ModuleTestHelperRefinement::USED_REFINEMENTS = Module.used_refinements diff --git a/test/stdlib/OpenSSL_test.rb b/test/stdlib/OpenSSL_test.rb index b767aad2ad..e018deb4e3 100644 --- a/test/stdlib/OpenSSL_test.rb +++ b/test/stdlib/OpenSSL_test.rb @@ -13,6 +13,38 @@ def openssl?(major = nil, minor = nil, fix = nil, patch = 0) end end +class OpenSSLBufferingTestIO + include OpenSSL::Buffering + + def initialize + @eof = true + @rbuffer = +"hello" + end +end + +class OpenSSLBufferingTest < Test::Unit::TestCase + include TestHelper + library "openssl" + testing "::OpenSSL::Buffering" + + def test_read + assert_send_type "(nil, nil) -> String?", + OpenSSLBufferingTestIO.new, :read, nil, nil + assert_send_type "(Integer, nil) -> String?", + OpenSSLBufferingTestIO.new, :read, 1, nil + end + + def test_read_nonblock + assert_send_type "(Integer, nil) -> String", + OpenSSLBufferingTestIO.new, :read_nonblock, 1, nil + end + + def test_readpartial + assert_send_type "(Integer, nil) -> String", + OpenSSLBufferingTestIO.new, :readpartial, 1, nil + end +end + class OpenSSLSingletonTest < Test::Unit::TestCase include TestHelper library "openssl" diff --git a/test/stdlib/StringIO_test.rb b/test/stdlib/StringIO_test.rb index 62745afc8b..37121ea52b 100644 --- a/test/stdlib/StringIO_test.rb +++ b/test/stdlib/StringIO_test.rb @@ -39,6 +39,23 @@ def test_gets end end +class StringIOSingletonTest < Test::Unit::TestCase + include TestHelper + + testing 'singleton(::StringIO)' + + def test_open + assert_send_type "() -> ::StringIO", + StringIO, :open + assert_send_type "(::String) -> ::StringIO", + StringIO, :open, "abc" + assert_send_type "(::String, ::String) -> ::StringIO", + StringIO, :open, "abc", "r" + assert_send_type "() { (::StringIO) -> ::Integer } -> ::Integer", + StringIO, :open do |io| io.write("a") end + end +end + class StringIOTypeTest < Test::Unit::TestCase include TestHelper @@ -51,6 +68,18 @@ def test_write io, :write, "a", "b" end + def test_read + assert_send_type "(nil, nil) -> ::String?", + StringIO.new("a"), :read, nil, nil + assert_send_type "(::Integer, nil) -> ::String?", + StringIO.new("a"), :read, 1, nil + end + + def test_readpartial + assert_send_type "(::int, nil) -> ::String", + StringIO.new("a"), :readpartial, 1, nil + end + def test_truncate io = StringIO.new @@ -85,4 +114,80 @@ def test_readlines assert_send_type "(::String sep, ::Integer limit, chomp: boolish) -> ::Array[::String]", StringIO.new("\n"), :readlines, "\n", 1, chomp: true end + + def test_pread + assert_send_type "(Integer, Integer) -> String", + StringIO.new("abcdef"), :pread, 3, 0 + assert_send_type "(Integer, Integer, String) -> String", + StringIO.new("abcdef"), :pread, 3, 0, +"buf" + assert_send_type "(::Integer, ::Integer, nil) -> ::String", + StringIO.new("a"), :pread, 1, 0, nil + end + + def test_read_nonblock + assert_send_type "(int) -> String", + StringIO.new("abc"), :read_nonblock, 2 + assert_send_type "(int, string) -> String", + StringIO.new("abc"), :read_nonblock, 2, +"buf" + assert_send_type "(int, exception: true) -> String", + StringIO.new("abc"), :read_nonblock, 2, exception: true + assert_send_type "(int, exception: false) -> String", + StringIO.new("abc"), :read_nonblock, 2, exception: false + assert_send_type "(int, exception: false) -> nil", + StringIO.new(""), :read_nonblock, 2, exception: false + assert_send_type "(::int, nil) -> ::String?", + StringIO.new("a"), :read_nonblock, 1, nil + end + + def test_write_nonblock + assert_send_type "(_ToS) -> Integer", + StringIO.new(+""), :write_nonblock, "abc" + assert_send_type "(_ToS, exception: true) -> Integer", + StringIO.new(+""), :write_nonblock, "abc", exception: true + assert_send_type "(_ToS, exception: false) -> Integer", + StringIO.new(+""), :write_nonblock, "abc", exception: false + end + + def test_sysread + assert_send_type "(Integer) -> String", + StringIO.new("abc"), :sysread, 2 + assert_send_type "(Integer, String) -> String", + StringIO.new("abc"), :sysread, 2, +"buf" + assert_send_type "(::Integer, nil) -> ::String", + StringIO.new("a"), :sysread, 1, nil + end + + def test_ungetc + assert_send_type "(String) -> nil", + StringIO.new(+"abc"), :ungetc, "x" + assert_send_type "(Integer) -> nil", + StringIO.new(+"abc"), :ungetc, 65 + end + + def test_set_encoding_by_bom + assert_send_type "() -> Encoding", + StringIO.new("\u{FEFF}abc"), :set_encoding_by_bom + assert_send_type "() -> nil", + StringIO.new("abc"), :set_encoding_by_bom + end + + def test_fcntl + assert_send_type_error "(*untyped) -> bot", NotImplementedError, + StringIO.new("abc"), :fcntl, 1, 1 + end + + def test_fsync + assert_send_type "() -> Integer", + StringIO.new("abc"), :fsync + end + + def test_internal_encoding + assert_send_type "() -> nil", + StringIO.new("abc"), :internal_encoding + end + + def test_external_encoding + assert_send_type "() -> Encoding", + StringIO.new("abc"), :external_encoding + end end diff --git a/test/stdlib/StringScanner_test.rb b/test/stdlib/StringScanner_test.rb new file mode 100644 index 0000000000..1a8b7cdce5 --- /dev/null +++ b/test/stdlib/StringScanner_test.rb @@ -0,0 +1,382 @@ +require_relative "test_helper" +require "strscan" + +class StringScannerSingletonTest < Test::Unit::TestCase + include TestHelper + + library "strscan" + testing "singleton(::StringScanner)" + + def test_new + assert_send_type "(::String) -> ::StringScanner", + StringScanner, :new, "foo" + assert_send_type "(::String, fixed_anchor: bool) -> ::StringScanner", + StringScanner, :new, "foo", fixed_anchor: true + end + + def test_must_C_version + assert_send_type "() -> singleton(::StringScanner)", + StringScanner, :must_C_version + end +end + +class StringScannerTest < Test::Unit::TestCase + include TestHelper + + library "strscan" + testing "::StringScanner" + + # A scanner that has already matched a named-capture regexp; every capture + # produced a non-nil String. Use this when you want the "all matched" branch. + def matched_named_scanner + s = StringScanner.new("Fri Dec 12 1975 14:39") + s.scan(/(?\w+) (?\w+) (?\d+) /) + s + end + + # A scanner where a named-capture regexp matched, but some captures were + # optional and resolved to nil. Use this when you want String? value cells. + def partial_named_scanner + s = StringScanner.new("Fri") + s.scan(/(?\w+)(? \w+)?/) + s + end + + # A fresh scanner that has not attempted any match yet. + def unmatched_scanner + StringScanner.new("Fri Dec 12 1975 14:39") + end + + def test_inspect + assert_send_type "() -> ::String", + StringScanner.new("foo"), :inspect + end + + def test_initialize_copy + assert_send_type "(::StringScanner) -> void", + StringScanner.new("foo"), :initialize_copy, StringScanner.new("bar") + end + + def test_left_shift + assert_send_type "(::String) -> ::StringScanner", + StringScanner.new("foo"), :<<, "bar" + end + + def test_square_bracket + s = matched_named_scanner + assert_send_type "(::Integer) -> ::String", + s, :[], 0 + assert_send_type "(::String) -> ::String", + s, :[], "wday" + assert_send_type "(::Symbol) -> ::String", + s, :[], :wday + assert_send_type "(::Integer) -> nil", + unmatched_scanner, :[], 0 + end + + def test_beginning_of_line? + assert_send_type "() -> bool", + StringScanner.new("foo"), :beginning_of_line? + end + + def test_bol? + assert_send_type "() -> bool", + StringScanner.new("foo"), :bol? + end + + def test_captures + assert_send_type "() -> ::Array[::String]", + matched_named_scanner, :captures + assert_send_type "() -> nil", + unmatched_scanner, :captures + end + + def test_charpos + assert_send_type "() -> ::Integer", + StringScanner.new("foo"), :charpos + end + + def test_check + assert_send_type "(::Regexp) -> ::String", + StringScanner.new("foo"), :check, /foo/ + assert_send_type "(::String) -> ::String", + StringScanner.new("foo"), :check, "foo" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foo"), :check, /nope/ + end + + def test_check_until + assert_send_type "(::Regexp) -> ::String", + StringScanner.new("foobar"), :check_until, /bar/ + assert_send_type "(::String) -> ::String", + StringScanner.new("foobar"), :check_until, "bar" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foobar"), :check_until, /nope/ + end + + def test_concat + assert_send_type "(::String) -> ::StringScanner", + StringScanner.new("foo"), :concat, "bar" + end + + def test_eos? + assert_send_type "() -> bool", + StringScanner.new("foo"), :eos? + end + + def test_exist? + assert_send_type "(::Regexp) -> ::Integer", + StringScanner.new("foobar"), :exist?, /bar/ + assert_send_type "(::String) -> ::Integer", + StringScanner.new("foobar"), :exist?, "bar" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foobar"), :exist?, /nope/ + end + + def test_fixed_anchor? + assert_send_type "() -> bool", + StringScanner.new("foo"), :fixed_anchor? + end + + def test_get_byte + assert_send_type "() -> ::String", + StringScanner.new("foo"), :get_byte + assert_send_type "() -> nil", + StringScanner.new(""), :get_byte + end + + def test_getch + assert_send_type "() -> ::String", + StringScanner.new("foo"), :getch + assert_send_type "() -> nil", + StringScanner.new(""), :getch + end + + def test_match? + assert_send_type "(::Regexp) -> ::Integer", + StringScanner.new("foo"), :match?, /foo/ + assert_send_type "(::String) -> ::Integer", + StringScanner.new("foo"), :match?, "foo" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foo"), :match?, /nope/ + end + + def test_matched + assert_send_type "() -> ::String", + matched_named_scanner, :matched + assert_send_type "() -> nil", + unmatched_scanner, :matched + end + + def test_matched? + assert_send_type "() -> bool", + matched_named_scanner, :matched? + assert_send_type "() -> bool", + unmatched_scanner, :matched? + end + + def test_matched_size + assert_send_type "() -> ::Integer", + matched_named_scanner, :matched_size + assert_send_type "() -> nil", + unmatched_scanner, :matched_size + end + + def test_named_captures + assert_send_type "() -> ::Hash[::String, ::String]", + matched_named_scanner, :named_captures + assert_send_type "() -> ::Hash[::String, ::String?]", + partial_named_scanner, :named_captures + end + + def test_peek + assert_send_type "(::Integer) -> ::String", + StringScanner.new("foo"), :peek, 3 + end + + def test_peek_byte + assert_send_type "() -> ::Integer", + StringScanner.new("foo"), :peek_byte + assert_send_type "() -> nil", + StringScanner.new(""), :peek_byte + end + + def test_pointer + assert_send_type "() -> ::Integer", + StringScanner.new("foo"), :pointer + end + + def test_pointer= + assert_send_type "(::Integer) -> ::Integer", + StringScanner.new("foo"), :pointer=, 0 + end + + def test_pos + assert_send_type "() -> ::Integer", + StringScanner.new("foo"), :pos + end + + def test_pos= + assert_send_type "(::Integer) -> ::Integer", + StringScanner.new("foo"), :pos=, 0 + end + + def test_post_match + assert_send_type "() -> ::String", + matched_named_scanner, :post_match + assert_send_type "() -> nil", + unmatched_scanner, :post_match + end + + def test_pre_match + assert_send_type "() -> ::String", + matched_named_scanner, :pre_match + assert_send_type "() -> nil", + unmatched_scanner, :pre_match + end + + def test_reset + assert_send_type "() -> ::StringScanner", + StringScanner.new("foo"), :reset + end + + def test_rest + assert_send_type "() -> ::String", + StringScanner.new("foo"), :rest + end + + def test_rest? + assert_send_type "() -> bool", + StringScanner.new("foo"), :rest? + end + + def test_rest_size + assert_send_type "() -> ::Integer", + StringScanner.new("foo"), :rest_size + end + + def test_scan + assert_send_type "(::Regexp) -> ::String", + StringScanner.new("foo"), :scan, /foo/ + assert_send_type "(::String) -> ::String", + StringScanner.new("foo"), :scan, "foo" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foo"), :scan, /nope/ + end + + def test_scan_byte + assert_send_type "() -> ::Integer", + StringScanner.new("foo"), :scan_byte + assert_send_type "() -> nil", + StringScanner.new(""), :scan_byte + end + + def test_scan_full + assert_send_type "(::Regexp, bool, true) -> ::String", + StringScanner.new("foo"), :scan_full, /foo/, true, true + assert_send_type "(::String, bool, true) -> ::String", + StringScanner.new("foo"), :scan_full, "foo", false, true + assert_send_type "(::Regexp, bool, true) -> nil", + StringScanner.new("foo"), :scan_full, /nope/, true, true + assert_send_type "(::Regexp, bool, false) -> ::Integer", + StringScanner.new("foo"), :scan_full, /foo/, true, false + assert_send_type "(::String, bool, false) -> ::Integer", + StringScanner.new("foo"), :scan_full, "foo", false, false + assert_send_type "(::Regexp, bool, false) -> nil", + StringScanner.new("foo"), :scan_full, /nope/, true, false + end + + def test_scan_integer + assert_send_type "() -> ::Integer", + StringScanner.new("123abc"), :scan_integer + assert_send_type "(base: ::Integer) -> ::Integer", + StringScanner.new("0xff"), :scan_integer, base: 16 + assert_send_type "() -> nil", + StringScanner.new("abc"), :scan_integer + end + + def test_scan_until + assert_send_type "(::Regexp) -> ::String", + StringScanner.new("foobar"), :scan_until, /bar/ + assert_send_type "(::String) -> ::String", + StringScanner.new("foobar"), :scan_until, "bar" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foobar"), :scan_until, /nope/ + end + + def test_search_full + assert_send_type "(::Regexp, bool, true) -> ::String", + StringScanner.new("foobar"), :search_full, /bar/, true, true + assert_send_type "(::String, bool, true) -> ::String", + StringScanner.new("foobar"), :search_full, "bar", false, true + assert_send_type "(::Regexp, bool, true) -> nil", + StringScanner.new("foobar"), :search_full, /nope/, true, true + assert_send_type "(::Regexp, bool, false) -> ::Integer", + StringScanner.new("foobar"), :search_full, /bar/, true, false + assert_send_type "(::String, bool, false) -> ::Integer", + StringScanner.new("foobar"), :search_full, "bar", false, false + assert_send_type "(::Regexp, bool, false) -> nil", + StringScanner.new("foobar"), :search_full, /nope/, true, false + end + + def test_size + assert_send_type "() -> ::Integer", + matched_named_scanner, :size + assert_send_type "() -> nil", + unmatched_scanner, :size + end + + def test_skip + assert_send_type "(::Regexp) -> ::Integer", + StringScanner.new("foo"), :skip, /foo/ + assert_send_type "(::String) -> ::Integer", + StringScanner.new("foo"), :skip, "foo" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foo"), :skip, /nope/ + end + + def test_skip_until + assert_send_type "(::Regexp) -> ::Integer", + StringScanner.new("foobar"), :skip_until, /bar/ + assert_send_type "(::String) -> ::Integer", + StringScanner.new("foobar"), :skip_until, "bar" + assert_send_type "(::Regexp) -> nil", + StringScanner.new("foobar"), :skip_until, /nope/ + end + + def test_string + assert_send_type "() -> ::String", + StringScanner.new("foo"), :string + end + + def test_string= + assert_send_type "(::String) -> ::String", + StringScanner.new("foo"), :string=, "bar" + end + + def test_terminate + assert_send_type "() -> ::StringScanner", + StringScanner.new("foo"), :terminate + end + + def test_unscan + # unscan raises StringScanner::Error unless there was a prior match, + # so we have to scan something first. + s = StringScanner.new("foo") + s.scan(/foo/) + assert_send_type "() -> ::StringScanner", + s, :unscan + end + + def test_values_at + s = matched_named_scanner + assert_send_type "(::Integer) -> ::Array[::String]", + s, :values_at, 0 + assert_send_type "(::String) -> ::Array[::String]", + s, :values_at, "wday" + assert_send_type "(::Symbol) -> ::Array[::String]", + s, :values_at, :wday + assert_send_type "(::Integer) -> nil", + unmatched_scanner, :values_at, 0 + end +end diff --git a/test/stdlib/constant_drift_test.rb b/test/stdlib/constant_drift_test.rb new file mode 100644 index 0000000000..4fb99075ad --- /dev/null +++ b/test/stdlib/constant_drift_test.rb @@ -0,0 +1,77 @@ +require_relative "test_helper" + +# Guards against "constant drift" between the RBS core signatures and the actual +# runtime, in both directions: +# +# * a constant declared in RBS but no longer defined by Ruby (e.g. +# `Float::ROUNDS`, removed in Ruby 3.0), and +# * a constant defined by Ruby but missing from RBS. +# +# Only platform- and build-invariant core classes are hard-gated here. Classes +# whose constant set legitimately varies by OS or build options (`Process`, +# `Socket`, `Errno`, `Signal`, `File::Constants`, `RbConfig`, `Etc`, ...) are +# intentionally excluded: their RBS declarations cannot match any single +# platform's runtime. `Object`/`BasicObject`/`Kernel` are excluded too, since +# every top-level class shows up under `Object.constants`. +class ConstantDriftTest < Test::Unit::TestCase + # Platform/build-invariant core classes and modules whose declared constant + # set must match the runtime exactly. + HARD_GATE = [ + Float, Integer, Numeric, Rational, Complex, + Math, Comparable, + String, Symbol, + Array, Hash, Range, Struct, + NilClass, TrueClass, FalseClass + ].freeze + + # Known, intentional exceptions keyed by "::Name" => [:CONST, ...]. Use this + # for build- or platform-conditional constants (and `private_constant`s) that + # are legitimately undeclared, so the gate stays green across CI platforms + # without being weakened elsewhere. + SKIP = { + # Defined only when Ruby is built with GMP (USE_GMP): present on the Linux + # CI build, absent on e.g. macOS. + "::Integer" => [:GMP_VERSION] + }.freeze + + def env + StdlibTest::DEFAULT_ENV + end + + # Constants declared directly under `type_name` in the loaded RBS environment + # (plain constants plus nested classes/modules and their aliases), matching + # what `Module#constants(false)` returns at runtime. + def rbs_constants(type_name) + prefix = "#{type_name}::" + names = [] + [env.constant_decls, env.class_decls, env.class_alias_decls].each do |store| + store.each_key do |tn| + s = tn.to_s + next unless s.start_with?(prefix) + + rest = s.delete_prefix(prefix) + names << rest.to_sym unless rest.include?("::") + end + end + names.uniq.sort + end + + HARD_GATE.each do |klass| + define_method(:"test_no_constant_drift_#{klass.name.gsub("::", "_")}") do + type_name = "::#{klass.name}" + skip = SKIP[type_name] || [] + runtime = (klass.constants(false) - skip).sort + declared = (rbs_constants(type_name) - skip).sort + + stale = declared - runtime + missing = runtime - declared + + assert_empty stale, + "RBS declares #{type_name} constants that no longer exist at runtime: #{stale.inspect}. " \ + "Remove them from the signature (or add to ConstantDriftTest::SKIP if intentional)." + assert_empty missing, + "Runtime defines #{type_name} constants missing from RBS: #{missing.inspect}. " \ + "Add them to the signature (or add to ConstantDriftTest::SKIP if intentional)." + end + end +end diff --git a/test/stdlib/test_helper.rb b/test/stdlib/test_helper.rb index 204cbdecc9..e9848e69c7 100644 --- a/test/stdlib/test_helper.rb +++ b/test/stdlib/test_helper.rb @@ -41,6 +41,15 @@ def if_ruby(range, skip: true) end module WithStdlibAliases + def with_round_mode(&block) + block.call(nil) + + %i[up down even].each do |mode| + block.call(mode) + with_string(mode.to_s, &block) + end + end + def with_timeout(seconds: 1, nanoseconds: 0) unless block_given? return RBS::UnitTest::WithAliases::WithEnum.new( @@ -148,6 +157,17 @@ def self.included(base) base.extend RBS::UnitTest::TypeAssertions::ClassMethods end + # Disables verbose mode for the duration of the passed block. Used when + # testing codepaths which emit warnings when run. + # + # The `verbose` variable can be used to change the verbosity if needed. + def disable_verbose(verbose = nil) + old_verbose, $VERBOSE = $VERBOSE, verbose + yield + ensure + $VERBOSE = old_verbose + end + RUBY_EXECUTABLE = ENV["RUBY"] || RbConfig.ruby def silence_warning(which) diff --git a/test/stdlib/zlib/GzipReader_test.rb b/test/stdlib/zlib/GzipReader_test.rb index 6d2e288b60..9eddc24dd3 100644 --- a/test/stdlib/zlib/GzipReader_test.rb +++ b/test/stdlib/zlib/GzipReader_test.rb @@ -29,7 +29,7 @@ def test_wrap path = File.join(tmpdir, "test.gz") Zlib::GzipWriter.open(path) { _1.puts "hello" } - File.open(path) do |io| + File.open(path, "rb") do |io| assert_send_type "(IO io) -> Zlib::GzipReader", Zlib::GzipReader, :wrap, io @@ -41,3 +41,33 @@ def test_wrap end end end + +class ZlibGzipReaderInstanceTest < Test::Unit::TestCase + include TestHelper + + library "zlib" + testing "::Zlib::GzipReader" + + def test_read + assert_send_type "(nil, nil) -> String?", + gzip_reader, :read, nil, nil + assert_send_type "(int length, nil) -> String?", + gzip_reader, :read, 1, nil + end + + def test_readpartial + assert_send_type "(int maxlen, nil) -> String", + gzip_reader, :readpartial, 1, nil + end + + private + + def gzip_reader + io = StringIO.new + writer = Zlib::GzipWriter.new(io) + writer.write("hello") + writer.close + + Zlib::GzipReader.new(StringIO.new(io.string)) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9a077b1c08..56bf68e6b5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -28,6 +28,55 @@ class Test::Unit::TestCase prepend TestSkip + + # Omit *all* test cases defined in this class when running on TruffleRuby. + # + # Call it at the class body level for features that are known not to work on + # TruffleRuby (e.g. those relying on `RubyVM::AbstractSyntaxTree` or + # `TracePoint` events that TruffleRuby does not implement): + # + # class RBS::RuntimePrototypeTest < Test::Unit::TestCase + # omit_on_truffle_ruby! "RubyVM::AbstractSyntaxTree is not available" + # # ... + # end + def self.omit_on_truffle_ruby!(reason = "Not supported on TruffleRuby") + return unless RUBY_ENGINE == "truffleruby" + + setup { omit(reason) } + end + + # Omit the running test case when running on TruffleRuby. + # + # Use it inside a test method when only a few cases of an otherwise supported + # class fail on TruffleRuby: + # + # def test_something + # omit_on_truffle_ruby! + # # ... + # end + def omit_on_truffle_ruby!(reason = "Not supported on TruffleRuby") + omit(reason) if RUBY_ENGINE == "truffleruby" + end + + # Omit *all* test cases defined in this class when running on JRuby. + # + # On JRuby the parser runs in WebAssembly (see lib/rbs/wasm). Use this at the + # class body level for features that depend on the C extension or on APIs JRuby + # does not implement. + def self.omit_on_jruby!(reason = "Not supported on JRuby") + return unless RUBY_ENGINE == "jruby" + + setup { omit(reason) } + end + + # Omit the running test case when running on JRuby. + # + # Use it inside a test method when only a few cases of an otherwise supported + # class fail on JRuby (e.g. those exercising parser features not yet wired + # through the WebAssembly bridge, such as `lex` or `parse_type_params`). + def omit_on_jruby!(reason = "Not supported on JRuby") + omit(reason) if RUBY_ENGINE == "jruby" + end end module TestHelper diff --git a/test/typecheck/hash_pair/Steepfile b/test/typecheck/hash_pair/Steepfile new file mode 100644 index 0000000000..77b2334655 --- /dev/null +++ b/test/typecheck/hash_pair/Steepfile @@ -0,0 +1,7 @@ +D = Steep::Diagnostic + +target :test do + signature "." + check "." + configure_code_diagnostics(D::Ruby.all_error) +end diff --git a/test/typecheck/hash_pair/test.rb b/test/typecheck/hash_pair/test.rb new file mode 100644 index 0000000000..89cc5912de --- /dev/null +++ b/test/typecheck/hash_pair/test.rb @@ -0,0 +1,37 @@ +class ToAry + def initialize (*x) @x = x end + def to_ary = @x +end + +class Pair + def initialize(x, y) @x, @y = x, y end + def to_ary = [@x, @y] +end + +class Test + def self.takes_pair(pair) + x, y = pair.to_ary + x + y + end +end + +is_pair = Pair.new(1, 2r) #: Pair[Integer, Rational] + +Test.takes_pair(is_pair) + + +Hash[[ + Pair.new(:a, 1r), + Pair.new(:b, 2r) +]] #: Hash[Symbol, Rational] + +Hash[ + ToAry.new( + Pair.new(:a, 1r), + Pair.new(:b, 2r) + ) +] #: Hash[Symbol, Rational] + +hash = { a: 3, b: 4 } +hash.to_h { |k, v| Pair.new(k.to_s, v.to_r) } #: Hash[String, Rational] + diff --git a/test/typecheck/hash_pair/test.rbs b/test/typecheck/hash_pair/test.rbs new file mode 100644 index 0000000000..c4e5a22b26 --- /dev/null +++ b/test/typecheck/hash_pair/test.rbs @@ -0,0 +1,18 @@ +class ToAry[X] + @x: Array[X] + def initialize: (*X) -> void + def to_ary: () -> Array[X] +end + +class Pair[X, Y] + @x: X + @y: Y + + def initialize: (X, Y) -> void + + def to_ary: () -> [X, Y] +end + +class Test + def self.takes_pair: (Hash::_Pair[Integer, Rational]) -> Rational +end diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000000..174a56a850 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,98 @@ +# RBS parser as WebAssembly + +The parser under [`src/`](../src) is plain, self-contained C with no dependency +on the Ruby C API, so it can be compiled to WebAssembly as-is. This directory +holds the small entry-point shim ([`rbs_wasm.c`](rbs_wasm.c)) that exposes a +stable ABI to a WebAssembly host. + +This is how RBS runs on Ruby implementations that cannot load the MRI C +extension (notably JRuby): the host loads `rbs_parser.wasm`, runs the parser over +a source buffer, and reads the serialized AST back out. The Ruby side then +rebuilds `RBS::AST` objects with `RBS::WASM::Deserializer` — no native build per +platform required. See [`lib/rbs/wasm`](../lib/rbs/wasm) and +[`docs/wasm_serialization.md`](../docs/wasm_serialization.md). + +## Building + +The build needs the [WASI SDK](https://github.com/WebAssembly/wasi-sdk/releases) +(for `clang`, the wasi-libc sysroot, and the wasm32 compiler-rt builtins): + +```console +$ export WASI_SDK_PATH=/path/to/wasi-sdk +$ rake wasm:build # compile rbs_parser.wasm +$ rake wasm:check # also smoke-test it (needs wasmtime) +$ rake wasm:jruby_setup # copy rbs_parser.wasm into lib/rbs/wasm/ for JRuby +$ rake wasm:install_jars # download the Chicory/ASM jars into ~/.m2 (run on JRuby) +``` + +The compiled `rbs_parser.wasm` is a build artifact and is not checked in. + +Like the MRI extension, the module is compiled with `-DNDEBUG`, which removes the +`RBS_ASSERT` checks — they sit in the lexer and the constant pool, so leaving them +in costs around 20% of parse time. `DEBUG=1 rake wasm:build` keeps them, which is +what you want when debugging the parser itself through the module. + +The WASI SDK is needed for the *build*, not for running the result — the host clang already +knows the `wasm32` target, but there is no wasm32 libc on a normal machine, so it picks up the +host headers and fails on the first `#include`. That is what the SDK supplies, along with the +builtins the link step needs. + +## Running the suite on JRuby + +[`Dockerfile.jruby`](../Dockerfile.jruby) builds an image that has everything this needs, so no +JRuby, JDK or WASI SDK has to be installed to work on the JRuby side: + +```console +$ docker build -f Dockerfile.jruby -t rbs-jruby . +$ docker run --rm rbs-jruby # run the test suite +$ docker run --rm -e RBS_PLATFORM=java rbs-jruby \ + gem build rbs.gemspec # build the -java gem +``` + +Two things in it are not obvious: + +- `build-essential` is for prism, which builds `libprism.so` and loads it through FFI on JRuby + rather than as an MRI C extension. It needs `cc` and `make`. +- Bundler is skipped. The development `Gemfile` pulls in CRuby-only C extensions (bigdecimal, + stackprof, …) that cannot build on JRuby, so the few gems the suite needs are installed + directly, in the same set as [`jruby.yml`](../.github/workflows/jruby.yml). + +The image compiles `rbs_parser.wasm` itself, which is why it carries the WASI SDK. That is not +the only arrangement: the build needs the SDK but not JRuby, and running the suite needs JRuby +but not the SDK, so `jruby.yml` splits them instead — it compiles the module on CRuby and then +switches engines to test against the result. + +`rake wasm:install_jars` is the step that has to be on JRuby either way: it resolves the `jar` +requirements from `rbs.gemspec` through the JVM. + +## Exported functions + +The module is built as a "reactor": it has no `main`, and the host calls +`_initialize` once before invoking any export. + +Memory management and results: + +| Export | Signature | Description | +| --- | --- | --- | +| `rbs_wasm_alloc` | `(i32) -> i32` | Allocate N bytes in linear memory, return the offset. | +| `rbs_wasm_free` | `(i32) -> ()` | Free a region from `rbs_wasm_alloc`. | +| `rbs_wasm_result_ptr` | `() -> i32` | Offset of the most recent result. | +| `rbs_wasm_result_len` | `() -> i32` | Length of the most recent result. | + +Parsing — each takes the whole buffer (`ptr`/`len`), its Ruby encoding name +(`enc`/`enc_len`, e.g. `"UTF-8"` or `"EUC-JP"`; falls back to UTF-8 when empty or +unknown), and the character range to parse (`start`/`end`). Each returns `1` on +success or `0` on a parse error. On success the result is the serialized AST; on +error it is an error blob (start/end positions, syntax flag, token type, +message). Type/method-type parsing also takes a buffer of newline-separated +type-variable names (`vars`/`vars_len`, with `vars_len < 0` meaning "none"): + +| Export | Signature | +| --- | --- | +| `rbs_wasm_parse_signature` | `(ptr, len, enc, enc_len, start, end) -> i32` | +| `rbs_wasm_parse_type` | `(ptr, len, enc, enc_len, start, end, vars, vars_len, require_eof, void_allowed, self_allowed, classish_allowed) -> i32` | +| `rbs_wasm_parse_method_type` | `(ptr, len, enc, enc_len, start, end, vars, vars_len, require_eof) -> i32` | +| `rbs_wasm_selftest` | `() -> i32` (parses a fixed sample; `1` on success) | + +For type and method-type parsing, a successful result of length 0 means the input +was empty (`nil`). diff --git a/wasm/rbs_wasm.c b/wasm/rbs_wasm.c new file mode 100644 index 0000000000..efe9a609c9 --- /dev/null +++ b/wasm/rbs_wasm.c @@ -0,0 +1,423 @@ +/** + * @file rbs_wasm.c + * + * WebAssembly entry points for the RBS parser. + * + * The parser in `src/` is plain, self-contained C with no dependency on the + * Ruby C API, so it compiles to WebAssembly as-is. This file exposes a small, + * stable ABI so the parser can be driven from a WebAssembly host (a JVM-based + * runtime running under JRuby). + * + * The flow is: the host writes a UTF-8 source string into linear memory + * (`rbs_wasm_alloc`), calls one of the `rbs_wasm_parse_*` functions, and reads + * the result back out (`rbs_wasm_result_ptr` / `rbs_wasm_result_len`). On + * success the result is the serialized AST (see `rbs_serialize_node` and + * `docs/wasm_serialization.md`); on a parse error it is an error blob (see + * `set_error_result`). `RBS::WASM` on the Ruby side decodes both. + * + * Built as a "reactor": no `main`, and the host calls `_initialize` once before + * invoking any export. + */ + +#include +#include +#include + +#include "rbs/parser.h" +#include "rbs/serialize.h" +#include "rbs/string.h" +#include "rbs/util/rbs_buffer.h" +#include "rbs/util/rbs_encoding.h" + +// The result of the most recent parse, living in linear memory until the next +// call replaces it. WebAssembly is little-endian, so the multi-byte integers +// written below match the little-endian format the Ruby decoder expects. +static char *result_buffer = NULL; +static int32_t result_length = 0; + +// Replace the current result with a fresh `length`-byte buffer and return a +// pointer to it for the caller to fill in. +static char *allocate_result(size_t length) { + free(result_buffer); + result_buffer = (char *) malloc(length == 0 ? 1 : length); + result_length = (int32_t) length; + return result_buffer; +} + +/** + * Allocate `size` bytes in linear memory and return the offset. The host uses + * this to reserve space for an input string before calling a parse function. + */ +__attribute__((export_name("rbs_wasm_alloc"))) void *rbs_wasm_alloc(size_t size) { + return malloc(size); +} + +/** + * Free a region previously returned by `rbs_wasm_alloc`. + */ +__attribute__((export_name("rbs_wasm_free"))) void rbs_wasm_free(void *ptr) { + free(ptr); +} + +/** + * Offset of the most recent parse result in linear memory. + */ +__attribute__((export_name("rbs_wasm_result_ptr"))) +int32_t +rbs_wasm_result_ptr(void) { + return (int32_t) (intptr_t) result_buffer; +} + +/** + * Length, in bytes, of the most recent parse result. + */ +__attribute__((export_name("rbs_wasm_result_len"))) +int32_t +rbs_wasm_result_len(void) { + return result_length; +} + +// Encode the parser's error into the result buffer: +// +// [i32 start_char][i32 end_char][u8 syntax_error] +// [u32 token_type_len][token_type bytes][u32 message_len][message bytes] +// +// Always returns 0, the failure status for the parse functions. +static int set_error_result(rbs_parser_t *parser) { + rbs_error_t *error = parser->error; + const char *token_type = rbs_token_type_str(error->token.type); + const char *message = error->message; + uint32_t token_type_len = (uint32_t) strlen(token_type); + uint32_t message_len = (uint32_t) strlen(message); + + int32_t start_char = error->token.range.start.char_pos; + int32_t end_char = error->token.range.end.char_pos; + uint8_t syntax_error = error->syntax_error ? 1 : 0; + + size_t total = 4 + 4 + 1 + 4 + token_type_len + 4 + message_len; + char *p = allocate_result(total); + + memcpy(p, &start_char, 4); + p += 4; + memcpy(p, &end_char, 4); + p += 4; + *p++ = (char) syntax_error; + memcpy(p, &token_type_len, 4); + p += 4; + memcpy(p, token_type, token_type_len); + p += token_type_len; + memcpy(p, &message_len, 4); + p += 4; + memcpy(p, message, message_len); + + return 0; +} + +static int set_serialized_result(rbs_parser_t *parser, rbs_node_t *node) { + rbs_string_t bytes = rbs_serialize_node(parser->allocator, &parser->constant_pool, node); + size_t length = rbs_string_len(bytes); + memcpy(allocate_result(length), bytes.start, length); + return 1; +} + +// A reversed or out-of-bounds range would make the lexer loop forever, which +// would hang the whole host. Hosts are expected to validate too (RBS::Parser +// raises on bad ranges), but guard here so a stray caller can never wedge the VM. +static bool range_is_valid(int start_pos, int end_pos, int length) { + return start_pos >= 0 && end_pos >= 0 && start_pos <= end_pos && end_pos <= length; +} + +// Resolve a Ruby encoding name (e.g. "UTF-8", "EUC-JP") to an rbs encoding, +// falling back to UTF-8 when none is given or the name is not recognised. +static const rbs_encoding_t *resolve_encoding(const char *name, int name_length) { + if (name_length > 0) { + const rbs_encoding_t *encoding = rbs_encoding_find((const uint8_t *) name, (const uint8_t *) (name + name_length)); + if (encoding != NULL) return encoding; + } + return RBS_ENCODING_UTF_8_ENTRY; +} + +// Declare type variables from a buffer of newline-separated names. A negative +// length means "no variables given" (the parser keeps its default table). +static void declare_variables(rbs_parser_t *parser, const char *variables, int variables_length) { + if (variables_length < 0) return; + + rbs_parser_push_typevar_table(parser, true); + + const char *cursor = variables; + const char *end = variables + variables_length; + const char *name_start = cursor; + + while (cursor <= end) { + if (cursor == end || *cursor == '\n') { + size_t name_length = (size_t) (cursor - name_start); + if (name_length > 0) { + uint8_t *copied = (uint8_t *) malloc(name_length); + memcpy(copied, name_start, name_length); + rbs_constant_id_t id = rbs_constant_pool_insert_owned(&parser->constant_pool, copied, name_length); + (void) rbs_parser_insert_typevar(parser, id); + } + name_start = cursor + 1; + } + cursor++; + } +} + +/** + * Parse an RBS signature from a source buffer. + * + * `source`/`length` is the whole buffer content; `encoding`/`encoding_length` is + * its Ruby encoding name; `start_pos`/`end_pos` are the character range within it + * to parse, so reported locations are absolute (this mirrors + * RBS::Parser._parse_signature). + * + * @return 1 on success (result is the serialized AST), 0 on a parse error + * (result is an error blob). + */ +__attribute__((export_name("rbs_wasm_parse_signature"))) int rbs_wasm_parse_signature(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos) { + if (!range_is_valid(start_pos, end_pos, length)) { + allocate_result(0); + return 0; + } + + rbs_string_t string = rbs_string_new(source, source + length); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + + rbs_signature_t *signature = NULL; + rbs_parse_signature(parser, &signature); + + int status; + if (parser->error == NULL) { + status = set_serialized_result(parser, (rbs_node_t *) signature); + } else { + status = set_error_result(parser); + } + + rbs_parser_free(parser); + return status; +} + +/** + * Parse a single RBS type. + * + * @param variables Newline-separated type variable names (length < 0 for none). + * @return 1 on success, 0 on a parse error. On success with an empty result + * (`rbs_wasm_result_len` == 0), the input was empty (`nil`). + */ +__attribute__((export_name("rbs_wasm_parse_type"))) int rbs_wasm_parse_type(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length, int require_eof, int void_allowed, int self_allowed, int classish_allowed) { + if (!range_is_valid(start_pos, end_pos, length)) { + allocate_result(0); + return 0; + } + + rbs_string_t string = rbs_string_new(source, source + length); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + declare_variables(parser, variables, variables_length); + + int status; + if (parser->next_token.type == pEOF) { + allocate_result(0); + status = 1; + } else { + rbs_node_t *type = NULL; + rbs_parse_type(parser, &type, void_allowed != 0, self_allowed != 0, classish_allowed != 0); + + if (parser->error == NULL && require_eof) { + rbs_parser_advance(parser); + if (parser->current_token.type != pEOF) { + rbs_parser_set_error(parser, parser->current_token, true, "expected a token `%s`", rbs_token_type_str(pEOF)); + } + } + + status = parser->error == NULL ? set_serialized_result(parser, type) : set_error_result(parser); + } + + rbs_parser_free(parser); + return status; +} + +/** + * Parse a single RBS method type. + * + * @param variables Newline-separated type variable names (length < 0 for none). + * @return 1 on success, 0 on a parse error. On success with an empty result, + * the input was empty (`nil`). + */ +__attribute__((export_name("rbs_wasm_parse_method_type"))) int rbs_wasm_parse_method_type(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length, int require_eof) { + if (!range_is_valid(start_pos, end_pos, length)) { + allocate_result(0); + return 0; + } + + rbs_string_t string = rbs_string_new(source, source + length); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + declare_variables(parser, variables, variables_length); + + int status; + if (parser->next_token.type == pEOF) { + allocate_result(0); + status = 1; + } else { + rbs_method_type_t *method_type = NULL; + rbs_parse_method_type(parser, &method_type, require_eof != 0, true); + + status = parser->error == NULL ? set_serialized_result(parser, (rbs_node_t *) method_type) : set_error_result(parser); + } + + rbs_parser_free(parser); + return status; +} + +/** + * Parse a type parameter list (e.g. `[T < Comparable]`). On success the result + * is a serialized node list; an empty result means the input was empty (`nil`). + */ +__attribute__((export_name("rbs_wasm_parse_type_params"))) int rbs_wasm_parse_type_params(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, int module_type_params) { + if (!range_is_valid(start_pos, end_pos, length)) { + allocate_result(0); + return 0; + } + + rbs_string_t string = rbs_string_new(source, source + length); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + + int status; + if (parser->next_token.type == pEOF) { + allocate_result(0); + status = 1; + } else { + rbs_node_list_t *params = NULL; + rbs_parse_type_params(parser, module_type_params != 0, ¶ms); + + if (parser->error == NULL) { + rbs_string_t bytes = rbs_serialize_node_list(parser->allocator, &parser->constant_pool, params); + size_t n = rbs_string_len(bytes); + memcpy(allocate_result(n), bytes.start, n); + status = 1; + } else { + status = set_error_result(parser); + } + } + + rbs_parser_free(parser); + return status; +} + +// Shared body for the leading/trailing inline annotation parsers. +static int parse_inline_annotation(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length, bool leading) { + if (!range_is_valid(start_pos, end_pos, length)) { + allocate_result(0); + return 0; + } + + rbs_string_t string = rbs_string_new(source, source + length); + rbs_parser_t *parser = rbs_parser_new(string, resolve_encoding(encoding, encoding_length), start_pos, end_pos); + declare_variables(parser, variables, variables_length); + + rbs_ast_ruby_annotations_t *annotation = NULL; + bool success = leading ? rbs_parse_inline_leading_annotation(parser, &annotation) : rbs_parse_inline_trailing_annotation(parser, &annotation); + + int status; + if (parser->error != NULL) { + status = set_error_result(parser); + } else if (!success || annotation == NULL) { + allocate_result(0); + status = 1; + } else { + status = set_serialized_result(parser, (rbs_node_t *) annotation); + } + + rbs_parser_free(parser); + return status; +} + +/** + * Parse an inline leading annotation. On success the result is a serialized + * node; an empty result means there was no annotation (`nil`). + */ +__attribute__((export_name("rbs_wasm_parse_inline_leading_annotation"))) int rbs_wasm_parse_inline_leading_annotation(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length) { + return parse_inline_annotation(source, length, encoding, encoding_length, start_pos, end_pos, variables, variables_length, true); +} + +/** + * Parse an inline trailing annotation. See rbs_wasm_parse_inline_leading_annotation. + */ +__attribute__((export_name("rbs_wasm_parse_inline_trailing_annotation"))) int rbs_wasm_parse_inline_trailing_annotation(const char *source, int length, const char *encoding, int encoding_length, int start_pos, int end_pos, const char *variables, int variables_length) { + return parse_inline_annotation(source, length, encoding, encoding_length, start_pos, end_pos, variables, variables_length, false); +} + +static void w_lex_u32(rbs_allocator_t *allocator, rbs_buffer_t *buffer, uint32_t value) { + unsigned char bytes[4] = { + (unsigned char) (value & 0xff), + (unsigned char) ((value >> 8) & 0xff), + (unsigned char) ((value >> 16) & 0xff), + (unsigned char) ((value >> 24) & 0xff), + }; + rbs_buffer_append_string(allocator, buffer, (const char *) bytes, 4); +} + +/** + * Lex the source into tokens. The result is a sequence of records, with no + * leading count (the host reads until the buffer is exhausted): + * + * [u32 type_name_len][type_name bytes][i32 start_char][i32 end_char] + * + * The final token is always pEOF, mirroring RBS::Parser._lex. + * + * @return 1 always (lexing does not report parse errors here). + */ +__attribute__((export_name("rbs_wasm_lex"))) int rbs_wasm_lex(const char *source, int length, const char *encoding, int encoding_length, int end_pos) { + rbs_allocator_t *allocator = rbs_allocator_init(); + rbs_lexer_t *lexer = rbs_lexer_new(allocator, rbs_string_new(source, source + length), resolve_encoding(encoding, encoding_length), 0, end_pos); + + rbs_buffer_t buffer; + rbs_buffer_init(allocator, &buffer); + + rbs_token_t token = NullToken; + while (token.type != pEOF) { + token = rbs_lexer_next_token(lexer); + + const char *type_name = rbs_token_type_str(token.type); + uint32_t type_name_length = (uint32_t) strlen(type_name); + w_lex_u32(allocator, &buffer, type_name_length); + rbs_buffer_append_string(allocator, &buffer, type_name, type_name_length); + w_lex_u32(allocator, &buffer, (uint32_t) token.range.start.char_pos); + w_lex_u32(allocator, &buffer, (uint32_t) token.range.end.char_pos); + } + + rbs_string_t bytes = rbs_buffer_to_string(&buffer); + size_t n = rbs_string_len(bytes); + memcpy(allocate_result(n), bytes.start, n); + + rbs_allocator_free(allocator); + return 1; +} + +/** + * Parse a small, fixed RBS document, used as a build smoke test + * (`wasmtime run --invoke rbs_wasm_selftest rbs_parser.wasm`). + * + * @return 1 if the sample parsed successfully, 0 otherwise. + */ +// Internal: defined in rbs_allocator.c, not declared in the public header. +extern size_t rbs_allocator_normalize_page_size(long raw); + +__attribute__((export_name("rbs_wasm_selftest"))) int rbs_wasm_selftest(void) { + // Regression test: normalize_page_size must return a safe value + // (>= sizeof(rbs_allocator_page_t)) for inputs that would underflow + // payload_size. On WASI in a Rust cdylib, sysconf(_SC_PAGESIZE) + // returns 0; the normalization must catch that. + if (rbs_allocator_normalize_page_size(-1) != 4096) return 0; + if (rbs_allocator_normalize_page_size(0) != 4096) return 0; + if (rbs_allocator_normalize_page_size(1) != 4096) return 0; + if (rbs_allocator_normalize_page_size(65536) != 65536) return 0; + + static const char source[] = + "class User\n" + " attr_reader name: String\n" + " def initialize: (String name) -> void\n" + "end\n"; + + int length = (int) (sizeof(source) - 1); + return rbs_wasm_parse_signature(source, length, "UTF-8", 5, 0, length); +}