diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..118a334b383a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +46a26945a172429740ebdd1fc83517130670080b diff --git a/.github/Dockerfile b/.github/Dockerfile index 3ba5466c30b8..e9b449421858 100644 --- a/.github/Dockerfile +++ b/.github/Dockerfile @@ -22,5 +22,5 @@ RUN apt-get update && \ # Install sbt ENV SBT_HOME /usr/local/sbt ENV PATH ${SBT_HOME}/bin:${PATH} -ENV SBT_VERSION 1.10.5 +ENV SBT_VERSION 1.10.7 RUN curl -sL "https://github.com/sbt/sbt/releases/download/v$SBT_VERSION/sbt-$SBT_VERSION.tgz" | gunzip | tar -x -C /usr/local \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9239020335e7..fdd3fa99f764 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -605,7 +605,7 @@ jobs: - name: Publish Nightly if: "steps.not_yet_published.outcome == 'success'" run: | - ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonatypeBundleRelease" + ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonaRelease" nightly_documentation: runs-on: [self-hosted, Linux] @@ -746,7 +746,7 @@ jobs: ./dist/target/sha256sum.txt - name: Publish Release - run: ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonatypeBundleUpload" + run: ./project/scripts/sbtPublish ";project scala3-bootstrapped ;publishSigned ;sonaUpload" open_issue_on_failure: @@ -782,14 +782,14 @@ jobs: uses: ./.github/workflows/build-chocolatey.yml needs: [ build-sdk-package ] with: - version: 3.3.5-local # unused - url : https://api.github.com/repos/scala/scala3/actions/artifacts/${{ needs.build-sdk-package.outputs.universal-id }}/zip - digest : ${{ needs.build-sdk-package.outputs.universal-digest }} + version: 3.3.7-SNAPSHOT # Fake version, used only for choco tests + url : https://api.github.com/repos/scala/scala3/actions/artifacts/${{ needs.build-sdk-package.outputs.win-x86_64-id }}/zip + digest : ${{ needs.build-sdk-package.outputs.win-x86_64-digest }} test-chocolatey-package: uses: ./.github/workflows/test-chocolatey.yml with: - version : 3.3.5-local # unused + version : 3.3.7-SNAPSHOT # Fake version, used only for choco tests java-version: 8 if: github.event_name == 'pull_request' && contains(github.event.pull_request.body, '[test_chocolatey]') needs: [ build-chocolatey-package ] diff --git a/.github/workflows/publish-chocolatey.yml b/.github/workflows/publish-chocolatey.yml index 3b31728a50ba..eef550e9d2f7 100644 --- a/.github/workflows/publish-chocolatey.yml +++ b/.github/workflows/publish-chocolatey.yml @@ -35,5 +35,4 @@ jobs: with: name: scala.nupkg - name: Publish the package to Chocolatey - run: choco push scala.nupkg --source https://push.chocolatey.org/ --api-key ${{ secrets.API-KEY }} - \ No newline at end of file + run: choco push scala.${{inputs.version}}.nupkg --source https://push.chocolatey.org/ --api-key ${{ secrets.API-KEY }} diff --git a/.github/workflows/test-chocolatey.yml b/.github/workflows/test-chocolatey.yml index b6ca9bf74b12..e302968b9129 100644 --- a/.github/workflows/test-chocolatey.yml +++ b/.github/workflows/test-chocolatey.yml @@ -21,7 +21,10 @@ on: env: CHOCOLATEY-REPOSITORY: chocolatey-pkgs - DOTTY_CI_INSTALLATION: ${{ secrets.GITHUB_TOKEN }} + # Controls behaviour of chocolatey{Install,Uninstall}.ps1 scripts + # During snapshot releases it uses a different layout and requires access token to GH Actions artifacts + # During stable releases it uses publically available archives + DOTTY_CI_INSTALLATION: ${{ endsWith(inputs.version, '-SNAPSHOT') && secrets.GITHUB_TOKEN || '' }} jobs: test: diff --git a/.jvmopts b/.jvmopts index a50abf36aa42..d8606eba733e 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,5 +1,5 @@ --Xss1m --Xms512m --Xmx4096m +-Xss2m +-Xms1024m +-Xmx8192m -XX:MaxInlineLevel=35 -XX:ReservedCodeCacheSize=512m diff --git a/bin/replQ b/bin/replQ new file mode 100755 index 000000000000..5d0b84c4a229 --- /dev/null +++ b/bin/replQ @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" >& /dev/null && pwd)/.." +. $ROOT/bin/commonQ + +java -Dscala.usejavacp=true -cp $cp dotty.tools.repl.Main -usejavacp "$@" diff --git a/changelogs/3.3.7-RC1.md b/changelogs/3.3.7-RC1.md new file mode 100644 index 000000000000..1a05d824ab95 --- /dev/null +++ b/changelogs/3.3.7-RC1.md @@ -0,0 +1,218 @@ +# Highlights of the release + +- Warn if interpolator uses toString [#20578](https://github.com/scala/scala3/pull/20578) +- Fixes #15736 blocking Scala 3 on Android [#22632](https://github.com/scala/scala3/pull/22632) +- Implement :jar (deprecate :require) [#22343](https://github.com/scala/scala3/pull/22343) +- In selector check, prefix of reference must match import qualifier [#20894](https://github.com/scala/scala3/pull/20894) +- Fix #21242: Add REPL flag to quit after evaluating init script [#22636](https://github.com/scala/scala3/pull/22636) +- Warn if implicit default shadows given [#23559](https://github.com/scala/scala3/pull/23559) + +# Other changes and fixes + +## Annotations + +- Approximate annotated types in `wildApprox` [#22893](https://github.com/scala/scala3/pull/22893) +- Fix copy of annotation on @main methods [#22582](https://github.com/scala/scala3/pull/22582) + +## CI + +- Disable Cats flaky tests [#23007](https://github.com/scala/scala3/pull/23007) + +## Developer Experience + +- Add support for running the `test` sub-command with the bisect script [#22796](https://github.com/scala/scala3/pull/22796) + +## Documentation + +- Improve the usage of inclusive language [#22360](https://github.com/scala/scala3/pull/22360) +- Update indentation.md to fix a typo [#23505](https://github.com/scala/scala3/pull/23505) + +## Enums + +- Make hashcode of enum items stable [#23218](https://github.com/scala/scala3/pull/23218) + +## Erasure + +- Add regression test for #23616 [#23623](https://github.com/scala/scala3/pull/23623) +- Disallow context function types as value-class parameters [#23015](https://github.com/scala/scala3/pull/23015) +- Handle type aliases in contextFunctionResultTypeAfter [#21517](https://github.com/scala/scala3/pull/21517) +- Align erasure of `Array[Nothing]` and `Array[Null]` with Scala 2 [#22517](https://github.com/scala/scala3/pull/22517) + +## Experimental: Erased definitions + +- Erased fields are not nullable [#23311](https://github.com/scala/scala3/pull/23311) + +## Implicits + +- Refine implicit search fallbacks for better ClassTag handling [#23532](https://github.com/scala/scala3/pull/23532) +- Fix #20335: Try extensions for arguments with type mismatch error [#23212](https://github.com/scala/scala3/pull/23212) + +## Inline + +- Fix Symbol.info remapping in TreeTypeMap [#23432](https://github.com/scala/scala3/pull/23432) + +## Lambda Lift + +- Fix: treat static vals as enclosures in lambdalift [#22452](https://github.com/scala/scala3/pull/22452) +- Fix: record calls to constructors in lambdaLift [#22487](https://github.com/scala/scala3/pull/22487) + +## Linting + +- Check OrType in interpolated toString lint [#23365](https://github.com/scala/scala3/pull/23365) +- Consider setter of effectively private var [#23211](https://github.com/scala/scala3/pull/23211) +- Nowarn receiver of extension taking params [#23351](https://github.com/scala/scala3/pull/23351) +- Dealias when looking into imports [#22889](https://github.com/scala/scala3/pull/22889) +- Revert unconditional lint of Inlined expansion [#22815](https://github.com/scala/scala3/pull/22815) +- Warn unused member of anonymous class [#22729](https://github.com/scala/scala3/pull/22729) +- No warning for parameter of overriding method [#22757](https://github.com/scala/scala3/pull/22757) +- Lazy val def member is pattern var [#22750](https://github.com/scala/scala3/pull/22750) +- Ignore params to default arg getters [#22749](https://github.com/scala/scala3/pull/22749) +- Restore resolving prefixes of implicit Ident [#22751](https://github.com/scala/scala3/pull/22751) +- Exclude synthetic this.m, Any.m from import lookup [#22695](https://github.com/scala/scala3/pull/22695) +- Nowarn public implicit val class params [#22664](https://github.com/scala/scala3/pull/22664) +- Don't warn retainedBody [#22510](https://github.com/scala/scala3/pull/22510) +- Suppress spurious Suppression [#22383](https://github.com/scala/scala3/pull/22383) +- Handle Typeable [#22663](https://github.com/scala/scala3/pull/22663) +- CheckUnused checks span.exists before testing its parts [#22504](https://github.com/scala/scala3/pull/22504) +- Process Export for unused check [#22984](https://github.com/scala/scala3/pull/22984) +- Enclosing package p.q not visible as q [#23069](https://github.com/scala/scala3/pull/23069) +- Remove premature caching of lookups for unused lint [#22982](https://github.com/scala/scala3/pull/22982) +- Improve checking LHS of Assign [#22977](https://github.com/scala/scala3/pull/22977) +- Improve Unit ascription escape hatch [#23147](https://github.com/scala/scala3/pull/23147) +- Mention extension in unused param warning [#23132](https://github.com/scala/scala3/pull/23132) +- Dealias for unused param check [#23256](https://github.com/scala/scala3/pull/23256) +- Take inferred or explicit refinement result for unused check [#23325](https://github.com/scala/scala3/pull/23325) +- Add accessible check for import usage [#23348](https://github.com/scala/scala3/pull/23348) +- Use result of lambda type of implicit in CheckUnused [#23497](https://github.com/scala/scala3/pull/23497) + +## Match Types + +- Handle NoType in TypeComparer.disjointnessBoundary [#21520](https://github.com/scala/scala3/pull/21520) +- Fix: #23261 Distinguish 0.0 and -0.0 in ConstantType match types [#23265](https://github.com/scala/scala3/pull/23265) + +## Metaprogramming + +- Add a check for correct Array shape in quotes.reflect.ClassOfConstant [#22033](https://github.com/scala/scala3/pull/22033) + +## Opaque Types + +- Fix stack overflow errors when generating opaque type proxies [#22479](https://github.com/scala/scala3/pull/22479) + +## Optional Braces + +- Correctly detect colon lambda eol indent for optional brace of argument [#22477](https://github.com/scala/scala3/pull/22477) + +## Overloading + +- Fail compilation if multiple conflicting top-level private defs/vals are in the same package [#22759](https://github.com/scala/scala3/pull/22759) + +## Parser + +- Allow observing an indent after conditional [#22611](https://github.com/scala/scala3/pull/22611) +- No outdent at eof [#22435](https://github.com/scala/scala3/pull/22435) +- Fix annotations being not expected in the middle of an array type by java parser [#22391](https://github.com/scala/scala3/pull/22391) +- Fix incorrect warning with -no-indent [#23216](https://github.com/scala/scala3/pull/23216) + +## Pattern Matching + +- Fix issue in lazy symbol completion or bug in nested classfile parser [#23634](https://github.com/scala/scala3/pull/23634) +- Fix existing GADT constraints with introduced pattern-bound symbols [#22928](https://github.com/scala/scala3/pull/22928) +- Avoid crash in uninhab check in Space [#22601](https://github.com/scala/scala3/pull/22601) + +## Pickling + +- Try to handle SkolemTypes in SingletonTypeTree during pickling [#23236](https://github.com/scala/scala3/pull/23236) + +## Positions + +- Compare span points in pathTo to determine best span [#23581](https://github.com/scala/scala3/pull/23581) + +## Presentation Compiler + +- Fix: Fix extracting refinements from intersection types in dynamic select hovers [#23640](https://github.com/scala/scala3/pull/23640) +- Completions for requests just before string [#22894](https://github.com/scala/scala3/pull/22894) +- Add enum type param support in sourceSymbol [#18603](https://github.com/scala/scala3/pull/18603) +- Use untpd.Tree instead of tpd.Tree for SelectionRangeProvider [#22702](https://github.com/scala/scala3/pull/22702) +- Fix: handle multiple params lists in for infer type [#23197](https://github.com/scala/scala3/pull/23197) +- Fix completion mode filtering + optimize scopeCompletions [#23172](https://github.com/scala/scala3/pull/23172) +- Add selection ranges for more names [#23257](https://github.com/scala/scala3/pull/23257) +- Add inlay hints for by-name parameters [#23283](https://github.com/scala/scala3/pull/23283) +- Add jpath to VirtualFile (for pc) [#23203](https://github.com/scala/scala3/pull/23203) + +## Quotes + +- Fix issue with static `this` references erroring in quoted code [#22618](https://github.com/scala/scala3/pull/22618) +- Skip splice level checking for symbols [#22782](https://github.com/scala/scala3/pull/22782) +- Fix stale top level synthetic package object being used in later runs [#23464](https://github.com/scala/scala3/pull/23464) + +## REPL + +- REPL: JLine 3.29.0 (was 3.27.1) [#22679](https://github.com/scala/scala3/pull/22679) +- Repl: emit warning for the `:sh` command [#22694](https://github.com/scala/scala3/pull/22694) + +## Reflection + +- Fix regression: do not approximate prefixes when using memberType in reflect API [#22448](https://github.com/scala/scala3/pull/22448) +- Forbid `StringConstant(null)` [#23064](https://github.com/scala/scala3/pull/23064) +- Quotes reflect: sort the typeMembers output list and filter out non-members [#22876](https://github.com/scala/scala3/pull/22876) + +## Reporting + +- Register nowarn when inlining [#22682](https://github.com/scala/scala3/pull/22682) +- Filter help renders box border [#22434](https://github.com/scala/scala3/pull/22434) +- Fix incorrect warning on type ascription for backquoted identifiers [#23088](https://github.com/scala/scala3/pull/23088) +- Add an explainer to the DoubleDefinition error [#23470](https://github.com/scala/scala3/pull/23470) + +## Scaladoc + +- Encode path of class [#23503](https://github.com/scala/scala3/pull/23503) + +## Settings + +- Chore: filter allowed source versions by import and by settings [#23215](https://github.com/scala/scala3/pull/23215) + +## Testing framework + +- Revert dubious retry in vulpix [#21801](https://github.com/scala/scala3/pull/21801) + +## Transform + +- Check only stable qual for import prefix [#22633](https://github.com/scala/scala3/pull/22633) +- Warn trivial recursion with module prefix [#23278](https://github.com/scala/scala3/pull/23278) + +## Tuples + +- Normalize tuple types in var args seq literals and classOf instances [#23465](https://github.com/scala/scala3/pull/23465) + +## Typer + +- Generalize "Don't approximate a type using Nothing as prefix" [#23628](https://github.com/scala/scala3/pull/23628) +- Don't approximate a type using `Nothing` as prefix [#23531](https://github.com/scala/scala3/pull/23531) +- Tighten condition to preserve denotation in IntegrateMap [#23060](https://github.com/scala/scala3/pull/23060) +- Disallow context bounds in type lambdas [#22659](https://github.com/scala/scala3/pull/22659) +- Fix #22724: Revert the PolyType case in #21744 [#22820](https://github.com/scala/scala3/pull/22820) +- Fix isGenericArrayElement for higher-kinded types [#22938](https://github.com/scala/scala3/pull/22938) +- Revert lambda cleanup [#22697](https://github.com/scala/scala3/pull/22697) +- Constructor companion gets privateWithin [#22627](https://github.com/scala/scala3/pull/22627) +- Add regression test for #22076 [#22602](https://github.com/scala/scala3/pull/22602) +- Constructor proxy is restricted if class is protected [#22563](https://github.com/scala/scala3/pull/22563) +- Check if a prefix is valid before selecting from a type [#22368](https://github.com/scala/scala3/pull/22368) +- Preserve hard unions in widenSingletons [#22369](https://github.com/scala/scala3/pull/22369) +- Revert recent changes to opaque type proxy generation [#23059](https://github.com/scala/scala3/pull/23059) +- Compare TypeVar and TypeParamRef in mergeRefinedOrApplied [#23045](https://github.com/scala/scala3/pull/23045) +- Only keep denotation for methods in IntegrateMap [#23226](https://github.com/scala/scala3/pull/23226) +- Tighten condition when to do SAM type conversion [#23246](https://github.com/scala/scala3/pull/23246) +- Revert "Make overload pruning based on result types less aggressive (#21744)" in main [#23331](https://github.com/scala/scala3/pull/23331) +- Fix #22922: Add TypeParamRef handling in isSingletonBounded [#23501](https://github.com/scala/scala3/pull/23501) +- Fix this references everywhere in dependent function types [#23514](https://github.com/scala/scala3/pull/23514) +- More careful ClassTag instantiation [#23659](https://github.com/scala/scala3/pull/23659) +- Use more context for implicit search only if no default argument [#23664](https://github.com/scala/scala3/pull/23664) + +## JDK + +- Check path of module prefix for tailrec [#23491](https://github.com/scala/scala3/pull/23491) + +## Releases + +- [CI] Switch releasing to Sontype Central instead of legacy Sonatype OSS [#23290](https://github.com/scala/scala3/pull/23290) + diff --git a/changelogs/3.3.7-RC2.md b/changelogs/3.3.7-RC2.md new file mode 100644 index 000000000000..dcee5f9ab7bd --- /dev/null +++ b/changelogs/3.3.7-RC2.md @@ -0,0 +1,19 @@ +# Backported fixes + +- Backport: Make coverage more similar to the one in Scala 2 [#23955](https://github.com/scala/scala3/pull/23955) +- Backport: latest Scala Presentation Improvements to 3.3.7 [#23945](https://github.com/scala/scala3/pull/23945). This includes [X-Ray Inlay Hints](https://github.com/scala/scala3/pull/23891), [using completions](https://github.com/scala/scala3/pull/23647), [do not add \[\] after `derives`](https://github.com/scala/scala3/pull/23811). +- Revert: "Check exhaustivity of any case class" to 3.3 LTS [#23943](https://github.com/scala/scala3/pull/23943) + +# Contributors + +Thank you to all the contributors who made this release possible 🎉 + +According to `git shortlog -sn --no-merges 3.3.7-RC1..3.3.7-RC2` these are: + +``` + 2 Tomasz Godzik + 1 Jan Chyb + 1 Vadim Chelyshov + 1 Zieliński Patryk + 1 vder +``` \ No newline at end of file diff --git a/changelogs/3.3.7.md b/changelogs/3.3.7.md new file mode 100644 index 000000000000..87f4220ef16c --- /dev/null +++ b/changelogs/3.3.7.md @@ -0,0 +1,283 @@ +# Highlights of the release + +- Warn a standard interpolator used toString on a reference type with `-Wtostring-interpolated` [#20578](https://github.com/scala/scala3/pull/20578) +- Unblock Scala 3 on Android [#22632](https://github.com/scala/scala3/pull/22632) +- Implement :jar (deprecate :require) in REPL [#22343](https://github.com/scala/scala3/pull/22343) +- Linting rework: In selector check, prefix of reference must match import qualifier [#20894](https://github.com/scala/scala3/pull/20894) +- Add REPL flag to quit after evaluating init script [#22636](https://github.com/scala/scala3/pull/22636) +- Warn if implicit default shadows given with `-Wrecurse-with-default` [#23559](https://github.com/scala/scala3/pull/23559) + +# Other changes and fixes + +## Annotations + +- Approximate annotated types in `wildApprox` [#22893](https://github.com/scala/scala3/pull/22893) +- Fix copy of annotation on @main methods [#22582](https://github.com/scala/scala3/pull/22582) + +## Coverage + +- Make coverage more similar to the one in Scala 2 [#23722](https://github.com/scala/scala3/pull/23722) + +## CI + +- Disable Cats flaky tests [#23007](https://github.com/scala/scala3/pull/23007) +- Switch releasing to Sontype Central instead of legacy Sonatype OSS [#23290](https://github.com/scala/scala3/pull/23290) +- Revert dubious retry in vulpix [#21801](https://github.com/scala/scala3/pull/21801) + +## Developer Experience + +- Add support for running the `test` sub-command with the bisect script [#22796](https://github.com/scala/scala3/pull/22796) + +## Documentation + +- Improve the usage of inclusive language [#22360](https://github.com/scala/scala3/pull/22360) +- Update indentation.md to fix a typo [#23505](https://github.com/scala/scala3/pull/23505) + +## Enums + +- Make hashcode of enum items stable [#23218](https://github.com/scala/scala3/pull/23218) + +## Erasure + +- Add regression test for [#23616](https://github.com/scala/scala3/issues/23616) [#23623](https://github.com/scala/scala3/pull/23623) +- Disallow context function types as value-class parameters to avoid crashes [#23015](https://github.com/scala/scala3/pull/23015) +- Handle type aliases in contextFunctionResultTypeAfter [#21517](https://github.com/scala/scala3/pull/21517) +- Align erasure of `Array[Nothing]` and `Array[Null]` with Scala 2 [#22517](https://github.com/scala/scala3/pull/22517) + +## Experimental: Erased definitions + +- Erased fields are not nullable [#23311](https://github.com/scala/scala3/pull/23311) + +## Implicits + +- Refine implicit search fallbacks for better ClassTag handling [#23532](https://github.com/scala/scala3/pull/23532) +- Try extensions for arguments with type mismatch error [#23212](https://github.com/scala/scala3/pull/23212) + +## Inline + +- Fix Symbol.info remapping in TreeTypeMap [#23432](https://github.com/scala/scala3/pull/23432) + +## Lambda Lift + +- Fix: treat static vals as enclosures in lambdalift [#22452](https://github.com/scala/scala3/pull/22452) +- Fix: record calls to constructors in lambdaLift [#22487](https://github.com/scala/scala3/pull/22487) + +## Linting + +- Check OrType in interpolated toString lint [#23365](https://github.com/scala/scala3/pull/23365) +- Consider setter of effectively private var [#23211](https://github.com/scala/scala3/pull/23211) +- Nowarn receiver of extension taking params [#23351](https://github.com/scala/scala3/pull/23351) +- Dealias when looking into imports [#22889](https://github.com/scala/scala3/pull/22889) +- Revert unconditional lint of Inlined expansion [#22815](https://github.com/scala/scala3/pull/22815) +- Warn unused member of anonymous class [#22729](https://github.com/scala/scala3/pull/22729) +- No warning for parameter of overriding method [#22757](https://github.com/scala/scala3/pull/22757) +- Lazy val def member is pattern var [#22750](https://github.com/scala/scala3/pull/22750) +- Ignore params to default arg getters [#22749](https://github.com/scala/scala3/pull/22749) +- Restore resolving prefixes of implicit Ident [#22751](https://github.com/scala/scala3/pull/22751) +- Exclude synthetic this.m, Any.m from import lookup [#22695](https://github.com/scala/scala3/pull/22695) +- Nowarn public implicit val class params [#22664](https://github.com/scala/scala3/pull/22664) +- Don't warn retainedBody [#22510](https://github.com/scala/scala3/pull/22510) +- Suppress spurious Suppression [#22383](https://github.com/scala/scala3/pull/22383) +- Handle Typeable [#22663](https://github.com/scala/scala3/pull/22663) +- CheckUnused checks span.exists before testing its parts [#22504](https://github.com/scala/scala3/pull/22504) +- Process Export for unused check [#22984](https://github.com/scala/scala3/pull/22984) +- Enclosing package p.q not visible as q [#23069](https://github.com/scala/scala3/pull/23069) +- Remove premature caching of lookups for unused lint [#22982](https://github.com/scala/scala3/pull/22982) +- Improve checking LHS of Assign [#22977](https://github.com/scala/scala3/pull/22977) +- Improve Unit ascription escape hatch [#23147](https://github.com/scala/scala3/pull/23147) +- Mention extension in unused param warning [#23132](https://github.com/scala/scala3/pull/23132) +- Dealias for unused param check [#23256](https://github.com/scala/scala3/pull/23256) +- Take inferred or explicit refinement result for unused check [#23325](https://github.com/scala/scala3/pull/23325) +- Add accessible check for import usage [#23348](https://github.com/scala/scala3/pull/23348) +- Use result of lambda type of implicit in CheckUnused [#23497](https://github.com/scala/scala3/pull/23497) + +## Match Types + +- Handle NoType in TypeComparer.disjointnessBoundary [#21520](https://github.com/scala/scala3/pull/21520) +- Distinguish 0.0 and -0.0 in ConstantType match types [#23265](https://github.com/scala/scala3/pull/23265) + +## Metaprogramming + +- Add a check for correct Array shape in quotes.reflect.ClassOfConstant [#22033](https://github.com/scala/scala3/pull/22033) + +## Opaque Types + +- Fix stack overflow errors when generating opaque type proxies [#22479](https://github.com/scala/scala3/pull/22479) + +## Optional Braces + +- Correctly detect colon lambda eol indent for optional brace of argument [#22477](https://github.com/scala/scala3/pull/22477) + +## Overloading + +- Fail compilation if multiple conflicting top-level private defs/vals are in the same package [#22759](https://github.com/scala/scala3/pull/22759) + +## Parser + +- Allow observing an indent after conditional [#22611](https://github.com/scala/scala3/pull/22611) +- No outdent at eof [#22435](https://github.com/scala/scala3/pull/22435) +- Fix annotations not expected in the middle of an array type by java parser [#22391](https://github.com/scala/scala3/pull/22391) +- Fix incorrect warning with -no-indent [#23216](https://github.com/scala/scala3/pull/23216) + +## Pattern Matching + +- Fix issue in lazy symbol completion or bug in nested classfile parser [#23634](https://github.com/scala/scala3/pull/23634) +- Fix existing GADT constraints with introduced pattern-bound symbols [#22928](https://github.com/scala/scala3/pull/22928) +- Avoid crash in uninhab check in Space [#22601](https://github.com/scala/scala3/pull/22601) + +## Pickling + +- Try to handle SkolemTypes in SingletonTypeTree during pickling [#23236](https://github.com/scala/scala3/pull/23236) + +## Presentation Compiler + +- Compare span points in pathTo to determine best span [#23581](https://github.com/scala/scala3/pull/23581) +- Fix: Fix extracting refinements from intersection types in dynamic select hovers [#23640](https://github.com/scala/scala3/pull/23640) +- Completions for requests just before string [#22894](https://github.com/scala/scala3/pull/22894) +- Add enum type param support in sourceSymbol [#18603](https://github.com/scala/scala3/pull/18603) +- Use untpd.Tree instead of tpd.Tree for SelectionRangeProvider [#22702](https://github.com/scala/scala3/pull/22702) +- Fix: handle multiple params lists in for infer type [#23197](https://github.com/scala/scala3/pull/23197) +- Fix completion mode filtering + optimize scopeCompletions [#23172](https://github.com/scala/scala3/pull/23172) +- Add selection ranges for more names [#23257](https://github.com/scala/scala3/pull/23257) +- Add inlay hints for by-name parameters [#23283](https://github.com/scala/scala3/pull/23283) +- Add jpath to VirtualFile (for pc) [#23203](https://github.com/scala/scala3/pull/23203) +- Add X-Ray Inlay Hints [#23891](https://github.com/scala/scala3/pull/23891) +- Add using to completions when applicable [#23647](https://github.com/scala/scala3/pull/23647) +- Do not add \[\] after `derives` [#23811](https://github.com/scala/scala3/pull/23811) + +## Quotes + +- Fix issue with static `this` references erroring in quoted code [#22618](https://github.com/scala/scala3/pull/22618) +- Skip splice level checking for symbols [#22782](https://github.com/scala/scala3/pull/22782) +- Fix stale top level synthetic package object being used in later runs [#23464](https://github.com/scala/scala3/pull/23464) + +## REPL + +- REPL: JLine 3.29.0 (was 3.27.1) [#22679](https://github.com/scala/scala3/pull/22679) +- REPL: emit warning for the `:sh` command [#22694](https://github.com/scala/scala3/pull/22694) + +## Reflection + +- Fix regression: do not approximate prefixes when using memberType in reflect API [#22448](https://github.com/scala/scala3/pull/22448) +- Forbid `StringConstant(null)` [#23064](https://github.com/scala/scala3/pull/23064) +- Quotes reflect: sort the typeMembers output list and filter out non-members [#22876](https://github.com/scala/scala3/pull/22876) + +## Reporting + +- Register nowarn when inlining [#22682](https://github.com/scala/scala3/pull/22682) +- Filter help renders box border [#22434](https://github.com/scala/scala3/pull/22434) +- Fix incorrect warning on type ascription for backquoted identifiers [#23088](https://github.com/scala/scala3/pull/23088) +- Add an explainer to the DoubleDefinition error [#23470](https://github.com/scala/scala3/pull/23470) + +## Scaladoc + +- Encode path of class [#23503](https://github.com/scala/scala3/pull/23503) + +## Settings + +- Chore: filter allowed source versions by import and by settings [#23215](https://github.com/scala/scala3/pull/23215) + +## Transform + +- Check only stable qual for import prefix [#22633](https://github.com/scala/scala3/pull/22633) +- Warn trivial recursion with module prefix [#23278](https://github.com/scala/scala3/pull/23278) + +## Tuples + +- Normalize tuple types in var args seq literals and classOf instances [#23465](https://github.com/scala/scala3/pull/23465) + +## Typer + +- Generalize "Don't approximate a type using Nothing as prefix" [#23628](https://github.com/scala/scala3/pull/23628) +- Don't approximate a type using `Nothing` as prefix [#23531](https://github.com/scala/scala3/pull/23531) +- Tighten condition to preserve denotation in IntegrateMap [#23060](https://github.com/scala/scala3/pull/23060) +- Disallow context bounds in type lambdas [#22659](https://github.com/scala/scala3/pull/22659) +- Revert the PolyType case in #21744 [#22820](https://github.com/scala/scala3/pull/22820) +- Fix isGenericArrayElement for higher-kinded types [#22938](https://github.com/scala/scala3/pull/22938) +- Revert lambda cleanup [#22697](https://github.com/scala/scala3/pull/22697) +- Constructor companion gets privateWithin [#22627](https://github.com/scala/scala3/pull/22627) +- Add regression test for #22076 [#22602](https://github.com/scala/scala3/pull/22602) +- Constructor proxy is restricted if class is protected [#22563](https://github.com/scala/scala3/pull/22563) +- Check if a prefix is valid before selecting from a type [#22368](https://github.com/scala/scala3/pull/22368) +- Preserve hard unions in widenSingletons [#22369](https://github.com/scala/scala3/pull/22369) +- Revert recent changes to opaque type proxy generation [#23059](https://github.com/scala/scala3/pull/23059) +- Compare TypeVar and TypeParamRef in mergeRefinedOrApplied [#23045](https://github.com/scala/scala3/pull/23045) +- Only keep denotation for methods in IntegrateMap [#23226](https://github.com/scala/scala3/pull/23226) +- Tighten condition when to do SAM type conversion [#23246](https://github.com/scala/scala3/pull/23246) +- Revert "Make overload pruning based on result types less aggressive (#21744)" in main [#23331](https://github.com/scala/scala3/pull/23331) +- Fix #22922: Add TypeParamRef handling in isSingletonBounded [#23501](https://github.com/scala/scala3/pull/23501) +- Fix this references everywhere in dependent function types [#23514](https://github.com/scala/scala3/pull/23514) +- More careful ClassTag instantiation [#23659](https://github.com/scala/scala3/pull/23659) +- Use more context for implicit search only if no default argument [#23664](https://github.com/scala/scala3/pull/23664) + +## JDK + +- Check path of module prefix for tailrec [#23491](https://github.com/scala/scala3/pull/23491) + + +# Contributors + +Thank you to all the contributors who made this release possible 🎉 + +According to `git shortlog -sn --no-merges 3.3.6..3.3.7` these are: + +``` + 164 Tomasz Godzik + 81 Som Snytt + 27 Jan Chyb + 25 Matt Bovel + 23 aherlihy + 20 kasiaMarek + 15 Martin Odersky + 13 Hamza Remmal + 11 Dale Wijnand + 11 noti0na1 + 10 Kacper Korban + 10 Wojciech Mazur + 8 Guillaume Martres + 7 Zieliński Patryk + 5 Jędrzej Rochala + 5 Quentin Bernet + 5 Seth Tisue + 4 Alexander + 4 Joel Wilsson + 4 Katarzyna Marek + 4 Oliver Bračevac + 3 Natsu Kagami + 3 Yoonjae Jeon + 3 anna herlihy + 2 Daisy Li + 2 Dolphin von Chips + 2 Mikołaj Fornal + 2 Sébastien Doeraene + 2 Vadim Chelyshov + 2 rochala + 1 Alec Theriault + 1 Aleksey Troitskiy + 1 Alex1005a + 1 Daniel Thoma + 1 Felix Herrmann + 1 HarrisL2 + 1 Jan + 1 Jan-Pieter van den Heuvel + 1 Jentsch + 1 João Ferreira + 1 Lukas Rytz + 1 Marc GRIS + 1 MatthieuSLR9 + 1 Nikita Glushchenko + 1 Niklas Fiekas + 1 Patryk Zieliński + 1 Piotr Chabelski + 1 Przemysław Sajnóg + 1 Rocco Mathijn Andela + 1 Tomas Mikula + 1 Yichen Xu + 1 bingchen-li + 1 fan-tom + 1 katrinafyi + 1 kijuky + 1 philippus + 1 philwalk + 1 vder +``` diff --git a/community-build/community-projects/cats b/community-build/community-projects/cats index 771c6c802f59..683f28dd0da4 160000 --- a/community-build/community-projects/cats +++ b/community-build/community-projects/cats @@ -1 +1 @@ -Subproject commit 771c6c802f59c72dbc1be1898081c9c882ddfeb0 +Subproject commit 683f28dd0da42e20c4bbf1515c7a7839c3d3c7a9 diff --git a/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala b/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala index b3065fefe87f..6aaaedb8a3dd 100644 --- a/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala +++ b/community-build/src/scala/dotty/communitybuild/CommunityBuildRunner.scala @@ -13,8 +13,7 @@ object CommunityBuildRunner: * is necessary since we run tests each time on a fresh * Docker container. We run the update on Docker container * creation time to create the cache of the dependencies - * and avoid network overhead. See https://github.com/lampepfl/dotty-drone - * for more infrastructural details. + * and avoid network overhead. */ extension (self: CommunityProject) def run()(using suite: CommunityBuildRunner): Unit = diff --git a/community-build/src/scala/dotty/communitybuild/projects.scala b/community-build/src/scala/dotty/communitybuild/projects.scala index c8e16d7df946..d8d791682cd3 100644 --- a/community-build/src/scala/dotty/communitybuild/projects.scala +++ b/community-build/src/scala/dotty/communitybuild/projects.scala @@ -128,7 +128,7 @@ final case class SbtCommunityProject( case Some(ivyHome) => List(s"-Dsbt.ivy.home=$ivyHome") case _ => Nil extraSbtArgs ++ sbtProps ++ List( - "-sbt-version", "1.10.5", + "-sbt-version", "1.10.7", "-Dsbt.supershell=false", s"-Ddotty.communitybuild.dir=$communitybuildDir", s"--addPluginSbtFile=$sbtPluginFilePath" diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala index 9c270482c6d4..836f5040b26e 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala @@ -1773,8 +1773,6 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { val returnUnit = lambdaTarget.info.resultType.typeSymbol == defn.UnitClass val functionalInterfaceDesc: String = generatedType.descriptor val desc = capturedParamsTypes.map(tpe => toTypeKind(tpe)).mkString(("("), "", ")") + functionalInterfaceDesc - // TODO specialization - val instantiatedMethodType = new MethodBType(lambdaParamTypes.map(p => toTypeKind(p)), toTypeKind(lambdaTarget.info.resultType)).toASMType val samMethod = atPhase(erasurePhase) { val samMethods = toDenot(functionalInterface).info.possibleSamMethods.toList @@ -1787,7 +1785,21 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { } val methodName = samMethod.javaSimpleName - val samMethodType = asmMethodType(samMethod).toASMType + val samMethodBType = asmMethodType(samMethod) + val samMethodType = samMethodBType.toASMType + + def boxInstantiated(instantiatedType: BType, samType: BType): BType = + if(!samType.isPrimitive && instantiatedType.isPrimitive) + boxedClassOfPrimitive(instantiatedType.asPrimitiveBType) + else instantiatedType + // TODO specialization + val instantiatedMethodBType = new MethodBType( + lambdaParamTypes.map(p => toTypeKind(p)), + boxInstantiated(toTypeKind(lambdaTarget.info.resultType), samMethodBType.returnType) + ) + + val instantiatedMethodType = instantiatedMethodBType.toASMType + // scala/bug#10334: make sure that a lambda object for `T => U` has a method `apply(T)U`, not only the `(Object)Object` // version. Using the lambda a structural type `{def apply(t: T): U}` causes a reflective lookup for this method. val needsGenericBridge = samMethodType != instantiatedMethodType diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index ef65b699d06d..955597261327 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -818,7 +818,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { methSymbol = dd.symbol jMethodName = methSymbol.javaSimpleName - returnType = asmMethodType(dd.symbol).returnType + returnType = asmMethodType(methSymbol).returnType isMethSymStaticCtor = methSymbol.isStaticConstructor resetMethodBookkeeping(dd) @@ -915,7 +915,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { for (p <- params) { emitLocalVarScope(p.symbol, veryFirstProgramPoint, onePastLastProgramPoint, force = true) } } - if (isMethSymStaticCtor) { appendToStaticCtor(dd) } + if (isMethSymStaticCtor) { appendToStaticCtor() } } // end of emitNormalMethodBody() lineNumber(rhs) @@ -936,7 +936,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { * * TODO document, explain interplay with `fabricateStaticInitAndroid()` */ - private def appendToStaticCtor(dd: DefDef): Unit = { + private def appendToStaticCtor(): Unit = { def insertBefore( location: asm.tree.AbstractInsnNode, diff --git a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala index 973e9cddcadd..1b16b2bc9800 100644 --- a/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/BackendUtils.scala @@ -186,6 +186,7 @@ object BackendUtils { 21 -> asm.Opcodes.V21, 22 -> asm.Opcodes.V22, 23 -> asm.Opcodes.V23, - 24 -> asm.Opcodes.V24 + 24 -> asm.Opcodes.V24, + 25 -> asm.Opcodes.V25 ) } diff --git a/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala b/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala index 536ab887a740..5dcafbef750f 100644 --- a/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala +++ b/compiler/src/dotty/tools/backend/jvm/ClassfileWriters.scala @@ -12,10 +12,10 @@ import java.util.zip.{CRC32, Deflater, ZipEntry, ZipOutputStream} import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Decorators.em +import dotty.tools.dotc.util.chaining.* import dotty.tools.io.{AbstractFile, PlainFile, VirtualFile} import dotty.tools.io.PlainFile.toPlainFile import BTypes.InternalName -import scala.util.chaining._ import dotty.tools.io.JarArchive import scala.language.unsafeNulls @@ -180,7 +180,7 @@ class ClassfileWriters(frontendAccess: PostProcessorFrontendAccess) { // important detail here, even on Windows, Zinc expects the separator within the jar // to be the system default, (even if in the actual jar file the entry always uses '/'). // see https://github.com/sbt/zinc/blob/dcddc1f9cfe542d738582c43f4840e17c053ce81/internal/compiler-bridge/src/main/scala/xsbt/JarUtils.scala#L47 - val pathInJar = + val pathInJar = if File.separatorChar == '/' then relativePath else relativePath.replace('/', File.separatorChar) PlainFile.toPlainFile(Paths.get(s"${file.absolutePath}!$pathInJar")) diff --git a/compiler/src/dotty/tools/backend/jvm/Primitives.scala b/compiler/src/dotty/tools/backend/jvm/Primitives.scala index c9ddfeab24e1..b173736b946c 100644 --- a/compiler/src/dotty/tools/backend/jvm/Primitives.scala +++ b/compiler/src/dotty/tools/backend/jvm/Primitives.scala @@ -150,13 +150,12 @@ object Primitives { case object XOR extends LogicalOp /** Signals the beginning of a series of concatenations. - * On the JVM platform, it should create a new StringBuffer - */ + * On the JVM platform, it should create a new StringBuilder. + */ case object StartConcat extends Primitive - /** - * type: (buf) => STR - * jvm : It should turn the StringBuffer into a String. + /** type: (buf) => STR + * jvm : It should turn the StringBuilder into a String. */ case object EndConcat extends Primitive diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 1572ed476c6c..4e63d31b6842 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -1496,7 +1496,7 @@ class JSCodeGen()(using genCtx: Context) { def jsParams = params.map(genParamDef(_)) - if (primitives.isPrimitive(sym)) { + if (primitives.isPrimitive(sym) && sym != defn.newArrayMethod) { None } else if (sym.is(Deferred) && currentClassSym.isNonNativeJSClass) { // scala-js/#4409: Do not emit abstract methods in non-native JS classes @@ -2553,6 +2553,8 @@ class JSCodeGen()(using genCtx: Context) { genCoercion(tree, receiver, code) else if (code == JSPrimitives.THROW) genThrow(tree, args) + else if (code == JSPrimitives.NEW_ARRAY) + genNewArray(tree, args) else if (JSPrimitives.isJSPrimitive(code)) genJSPrimitive(tree, args, code, isStat) else @@ -3022,6 +3024,24 @@ class JSCodeGen()(using genCtx: Context) { } } + /** Gen a call to the special `newArray` method. */ + private def genNewArray(tree: Apply, args: List[Tree]): js.Tree = { + implicit val pos: SourcePosition = tree.sourcePos + + val List(elemClazz, Literal(arrayClassConstant), dimsArray: JavaSeqLiteral) = args: @unchecked + + dimsArray.elems match { + case singleDim :: Nil => + // Use a js.NewArray + val arrayTypeRef = toTypeRef(arrayClassConstant.typeValue).asInstanceOf[jstpe.ArrayTypeRef] + js.NewArray(arrayTypeRef, List(genExpr(singleDim))) + case _ => + // Delegate to jlr.Array.newInstance + js.ApplyStatic(js.ApplyFlags.empty, JLRArrayClassName, js.MethodIdent(JLRArrayNewInstanceMethodName), + List(genExpr(elemClazz), genJavaSeqLiteral(dimsArray)))(jstpe.AnyType) + } + } + /** Gen a "normal" apply (to a true method). * * But even these are further refined into: @@ -4835,6 +4855,7 @@ class JSCodeGen()(using genCtx: Context) { object JSCodeGen { private val NullPointerExceptionClass = ClassName("java.lang.NullPointerException") + private val JLRArrayClassName = ClassName("java.lang.reflect.Array") private val JSObjectClassName = ClassName("scala.scalajs.js.Object") private val JavaScriptExceptionClassName = ClassName("scala.scalajs.js.JavaScriptException") @@ -4844,6 +4865,9 @@ object JSCodeGen { private val selectedValueMethodName = MethodName("selectedValue", Nil, ObjectClassRef) + private val JLRArrayNewInstanceMethodName = + MethodName("newInstance", List(jstpe.ClassRef(jsNames.ClassClass), jstpe.ArrayTypeRef(jstpe.IntRef, 1)), ObjectClassRef) + private val ObjectArgConstructorName = MethodName.constructor(List(ObjectClassRef)) private val thisOriginalName = OriginalName("this") diff --git a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala index a3a37795826a..f23c816b5204 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala @@ -48,9 +48,10 @@ object JSPrimitives { inline val UNWRAP_FROM_THROWABLE = WRAP_AS_THROWABLE + 1 // js.special.unwrapFromThrowable inline val DEBUGGER = UNWRAP_FROM_THROWABLE + 1 // js.special.debugger - inline val THROW = DEBUGGER + 1 + inline val THROW = DEBUGGER + 1 // .throw + inline val NEW_ARRAY = THROW + 1 // scala.runtime.Arrays.newArray - inline val UNION_FROM = THROW + 1 // js.|.from + inline val UNION_FROM = NEW_ARRAY + 1 // js.|.from inline val UNION_FROM_TYPE_CONSTRUCTOR = UNION_FROM + 1 // js.|.fromTypeConstructor inline val REFLECT_SELECTABLE_SELECTDYN = UNION_FROM_TYPE_CONSTRUCTOR + 1 // scala.reflect.Selectable.selectDynamic @@ -137,6 +138,7 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) { addPrimitive(jsdefn.Special_debugger, DEBUGGER) addPrimitive(defn.throwMethod, THROW) + addPrimitive(defn.newArrayMethod, NEW_ARRAY) addPrimitive(jsdefn.PseudoUnion_from, UNION_FROM) addPrimitive(jsdefn.PseudoUnion_fromTypeConstructor, UNION_FROM_TYPE_CONSTRUCTOR) diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index b796b6c3cb5b..e0c7e63be261 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -134,7 +134,7 @@ object CompilationUnit { /** Create a compilation unit corresponding to an in-memory String. * Used for `compiletime.testing.typeChecks`. */ - def apply(name: String, source: String)(using Context): CompilationUnit = { + def apply(name: String, source: String): CompilationUnit = { val src = SourceFile.virtual(name = name, content = source, maybeIncomplete = false) new CompilationUnit(src) } diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index b3757a9ddce7..c888dabdd61f 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -8,7 +8,6 @@ import cc.CheckCaptures import parsing.Parser import Phases.Phase import transform.* -import dotty.tools.backend import backend.jvm.{CollectSuperCalls, GenBCode} import localopt.StringInterpolatorOpt @@ -35,8 +34,7 @@ class Compiler { protected def frontendPhases: List[List[Phase]] = List(new Parser) :: // Compiler frontend: scanner, parser List(new TyperPhase) :: // Compiler frontend: namer, typer - List(new CheckUnused.PostTyper) :: // Check for unused elements - List(new CheckShadowing) :: // Check shadowing elements + List(CheckUnused.PostTyper(), CheckShadowing()) :: // Check for unused, shadowed elements List(new YCheckPositions) :: // YCheck positions List(new sbt.ExtractDependencies) :: // Sends information on classes' dependencies to sbt via callbacks List(new semanticdb.ExtractSemanticDB.ExtractSemanticInfo) :: // Extract info into .semanticdb files @@ -51,10 +49,10 @@ class Compiler { List(new Pickler) :: // Generate TASTY info List(new Inlining) :: // Inline and execute macros List(new PostInlining) :: // Add mirror support for inlined code - List(new CheckUnused.PostInlining) :: // Check for unused elements List(new Staging) :: // Check staging levels and heal staged types List(new Splicing) :: // Replace level 1 splices with holes List(new PickleQuotes) :: // Turn quoted trees into explicit run-time data structures + List(new CheckUnused.PostInlining) :: // Check for unused elements Nil /** Phases dealing with the transformation from pickled trees to backend trees */ diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 8e98f276a7c2..8b2af2d06316 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -17,26 +17,28 @@ import Phases.{unfusedPhases, Phase} import sbt.interfaces.ProgressCallback import util.* -import reporting.{Suppression, Action, Profile, ActiveProfile, NoProfile} -import reporting.Diagnostic -import reporting.Diagnostic.Warning +import reporting.{Suppression, Action, Profile, ActiveProfile, MessageFilter, NoProfile, WConf} +import reporting.Diagnostic, Diagnostic.Warning import rewrites.Rewrites import profile.Profiler import printing.XprintMode import typer.ImplicitRunInfo import config.Feature import StdNames.nme +import Spans.Span import java.io.{BufferedWriter, OutputStreamWriter} import java.nio.charset.StandardCharsets -import scala.collection.mutable +import scala.collection.mutable, mutable.ListBuffer import scala.util.control.NonFatal import scala.io.Codec import Run.Progress import scala.compiletime.uninitialized import dotty.tools.dotc.transform.MegaPhase +import dotty.tools.dotc.util.chaining.* +import java.util.{Timer, TimerTask} /** A compiler run. Exports various methods to compile source files */ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with ConstraintRunInfo { @@ -68,7 +70,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint private var myFiles: Set[AbstractFile] = _ // `@nowarn` annotations by source file, populated during typer - private val mySuppressions: mutable.LinkedHashMap[SourceFile, mutable.ListBuffer[Suppression]] = mutable.LinkedHashMap.empty + private val mySuppressions: mutable.LinkedHashMap[SourceFile, ListBuffer[Suppression]] = mutable.LinkedHashMap.empty // source files whose `@nowarn` annotations are processed private val mySuppressionsComplete: mutable.Set[SourceFile] = mutable.Set.empty // warnings issued before a source file's `@nowarn` annotations are processed, suspended so that `@nowarn` can filter them @@ -88,18 +90,44 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint mySuspendedMessages.getOrElseUpdate(warning.pos.source, mutable.LinkedHashSet.empty) += warning def nowarnAction(dia: Diagnostic): Action.Warning.type | Action.Verbose.type | Action.Silent.type = - mySuppressions.getOrElse(dia.pos.source, Nil).find(_.matches(dia)) match { - case Some(s) => + mySuppressions.get(dia.pos.source) match + case Some(suppressions) => + val matching = suppressions.iterator.filter(_.matches(dia)) + if matching.hasNext then + val s = matching.next() + for other <- matching do + if !other.used then + other.markSuperseded() // superseded unless marked used later s.markUsed() - if (s.verbose) Action.Verbose + if s.verbose then Action.Verbose else Action.Silent - case _ => + else Action.Warning - } + case none => + Action.Warning + + def registerNowarn(annotPos: SourcePosition, range: Span)(conf: String, pos: SrcPos)(using Context): Unit = + var verbose = false + val filters = conf match + case "" => + List(MessageFilter.Any) + case "none" => + List(MessageFilter.None) + case "verbose" | "v" => + verbose = true + List(MessageFilter.Any) + case conf => + WConf.parseFilters(conf).left.map: parseErrors => + report.warning(s"Invalid message filter\n${parseErrors.mkString("\n")}", pos) + List(MessageFilter.None) + .merge + addSuppression: + Suppression(annotPos, filters, range.start, range.end, verbose) def addSuppression(sup: Suppression): Unit = - val source = sup.annotPos.source - mySuppressions.getOrElseUpdate(source, mutable.ListBuffer.empty) += sup + val suppressions = mySuppressions.getOrElseUpdate(sup.annotPos.source, ListBuffer.empty) + if sup.start != sup.end then + suppressions += sup def reportSuspendedMessages(source: SourceFile)(using Context): Unit = { // sort suppressions. they are not added in any particular order because of lazy type completion @@ -109,17 +137,25 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint mySuspendedMessages.remove(source).foreach(_.foreach(ctx.reporter.issueIfNotSuppressed)) } - def runFinished(hasErrors: Boolean): Unit = + def runFinished()(using Context): Unit = + val hasErrors = ctx.reporter.hasErrors // report suspended messages (in case the run finished before typer) mySuspendedMessages.keysIterator.toList.foreach(reportSuspendedMessages) // report unused nowarns only if all all phases are done if !hasErrors && ctx.settings.WunusedHas.nowarn then - for { + for source <- mySuppressions.keysIterator.toList sups <- mySuppressions.remove(source) - sup <- sups.reverse - } if (!sup.used) - report.warning("@nowarn annotation does not suppress any warnings", sup.annotPos) + do + val suppressions = sups.reverse.toList + for sup <- suppressions do + if !sup.used + && !suppressions.exists(s => s.ne(sup) && s.used && s.annotPos == sup.annotPos) // duplicate + && sup.filters != List(MessageFilter.None) // invalid suppression, don't report as unused + then + val more = if sup.superseded then " but matches a diagnostic" else "" + report.warning("@nowarn annotation does not suppress any warnings"+more, sup.annotPos) + end suppressions /** The compilation units currently being compiled, this may return different * results over time. @@ -167,7 +203,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint val staticRefs = util.EqHashMap[Name, Denotation](initialCapacity = 1024) /** Actions that need to be performed at the end of the current compilation run */ - private var finalizeActions = mutable.ListBuffer[() => Unit]() + private var finalizeActions = ListBuffer.empty[() => Unit] private var _progress: Progress | Null = null // Set if progress reporting is enabled @@ -314,7 +350,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint profiler.onPhase(phase): try units = phase.runOn(units) catch case _: InterruptedException => cancelInterrupted() - if (ctx.settings.Xprint.value.containsPhase(phase)) + if (ctx.settings.Vprint.value.containsPhase(phase)) for (unit <- units) lastPrintedTree = printTree(lastPrintedTree)(using ctx.fresh.setPhase(phase.next).setCompilationUnit(unit)) @@ -348,7 +384,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint runPhases(allPhases = fusedPhases)(using runCtx) if (!ctx.reporter.hasErrors) Rewrites.writeBack() - suppressions.runFinished(hasErrors = ctx.reporter.hasErrors) + suppressions.runFinished() while (finalizeActions.nonEmpty && canProgress()) { val action = finalizeActions.remove(0) action() diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 9db12e21913a..197d0d081f7f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -14,11 +14,11 @@ import config.Feature.{sourceVersion, migrateTo3, enabled} import config.SourceVersion.* import collection.mutable.ListBuffer import reporting.* -import annotation.constructorOnly import printing.Formatting.hl import config.Printers import scala.annotation.internal.sharable +import scala.annotation.{unchecked as _, *} import dotty.tools.dotc.util.SrcPos object desugar { @@ -45,6 +45,14 @@ object desugar { */ val UntupledParam: Property.Key[Unit] = Property.StickyKey() + /** An attachment key to indicate that a ValDef originated from a pattern. + */ + val PatternVar: Property.Key[Unit] = Property.StickyKey() + + /** An attachment key for Trees originating in for-comprehension, such as tupling of assignments. + */ + val ForArtifact: Property.Key[Unit] = Property.StickyKey() + /** What static check should be applied to a Match? */ enum MatchCheck { case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom @@ -330,6 +338,7 @@ object desugar { .withMods(Modifiers( meth.mods.flags & (AccessFlags | Synthetic) | (vparam.mods.flags & Inline), meth.mods.privateWithin)) + .withSpan(vparam.rhs.span) val rest = defaultGetters(vparams :: paramss1, n + 1) if vparam.rhs.isEmpty then rest else defaultGetter :: rest case _ :: paramss1 => // skip empty parameter lists and type parameters @@ -1221,7 +1230,7 @@ object desugar { val matchExpr = if (tupleOptimizable) rhs else - val caseDef = CaseDef(pat, EmptyTree, makeTuple(ids)) + val caseDef = CaseDef(pat, EmptyTree, makeTuple(ids).withAttachment(ForArtifact, ())) Match(makeSelector(rhs, MatchCheck.IrrefutablePatDef), caseDef :: Nil) vars match { case Nil if !mods.is(Lazy) => @@ -1246,11 +1255,13 @@ object desugar { DefDef(named.name.asTermName, Nil, tpt, selector(n)) .withMods(mods &~ Lazy) .withSpan(named.span) + .withAttachment(PatternVar, ()) else valDef( ValDef(named.name.asTermName, tpt, selector(n)) .withMods(mods) .withSpan(named.span) + .withAttachment(PatternVar, ()) ) flatTree(firstDef :: restDefs) } @@ -1476,18 +1487,18 @@ object desugar { /** Map n-ary function `(x1: T1, ..., xn: Tn) => body` where n != 1 to unary function as follows: * * (x$1: (T1, ..., Tn)) => { - * def x1: T1 = x$1._1 + * val x1: T1 = x$1._1 * ... - * def xn: Tn = x$1._n + * val xn: Tn = x$1._n * body * } * * or if `isGenericTuple` * * (x$1: (T1, ... Tn) => { - * def x1: T1 = x$1.apply(0) + * val x1: T1 = x$1.apply(0) * ... - * def xn: Tn = x$1.apply(n-1) + * val xn: Tn = x$1.apply(n-1) * body * } * @@ -1542,10 +1553,12 @@ object desugar { val vdef = ValDef(named.name.asTermName, tpt, rhs) .withMods(mods) .withSpan(original.span.withPoint(named.span.start)) + .withAttachment(PatternVar, ()) val mayNeedSetter = valDef(vdef) mayNeedSetter } + @unused private def derivedDefDef(original: Tree, named: NameTree, tpt: Tree, rhs: Tree, mods: Modifiers)(implicit src: SourceFile) = DefDef(named.name.asTermName, Nil, tpt, rhs) .withMods(mods) @@ -1616,7 +1629,19 @@ object desugar { val matchCheckMode = if (gen.checkMode == GenCheckMode.Check || gen.checkMode == GenCheckMode.CheckAndFilter) MatchCheck.IrrefutableGenFrom else MatchCheck.None - makeCaseLambda(CaseDef(gen.pat, EmptyTree, body) :: Nil, matchCheckMode) + val pat = gen.pat.match + case Tuple(pats) if pats.length > Definitions.MaxImplementedFunctionArity => + /* The pattern case is a tupleXXL, because we have bound > 21 variables in the comprehension. + * In this case, we need to mark all the typed patterns as @unchecked, or get loads of warnings. + * Cf. warn test i23164.scala */ + Tuple: + pats.map: + case t @ Bind(name, tp @ Typed(id, tpt)) => + val annotated = Annotated(tpt, New(ref(defn.UncheckedAnnot.typeRef))) + cpy.Bind(t)(name, cpy.Typed(tp)(id, annotated)).withMods(t.mods) + case t => t + case _ => gen.pat + makeCaseLambda(CaseDef(pat, EmptyTree, body) :: Nil, matchCheckMode) } /** If `pat` is not an Identifier, a Typed(Ident, _), or a Bind, wrap @@ -1733,7 +1758,7 @@ object desugar { case _ => Modifiers() makePatDef(valeq, mods, defpat, rhs) } - val rhs1 = makeFor(nme.map, nme.flatMap, GenFrom(defpat0, gen.expr, gen.checkMode) :: Nil, Block(pdefs, makeTuple(id0 :: ids))) + val rhs1 = makeFor(nme.map, nme.flatMap, GenFrom(defpat0, gen.expr, gen.checkMode) :: Nil, Block(pdefs, makeTuple(id0 :: ids).withAttachment(ForArtifact, ()))) val allpats = gen.pat :: pats val vfrom1 = GenFrom(makeTuple(allpats), rhs1, GenCheckMode.Ignore) makeFor(mapName, flatMapName, vfrom1 :: rest1, body) diff --git a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala index 8ee75cbf364b..d04dd82fb424 100644 --- a/compiler/src/dotty/tools/dotc/ast/MainProxies.scala +++ b/compiler/src/dotty/tools/dotc/ast/MainProxies.scala @@ -103,17 +103,17 @@ object MainProxies { val body = Try(call, handler :: Nil, EmptyTree) val mainArg = ValDef(nme.args, TypeTree(defn.ArrayType.appliedTo(defn.StringType)), EmptyTree) .withFlags(Param) - /** Replace typed `Ident`s that have been typed with a TypeSplice with the reference to the symbol. - * The annotations will be retype-checked in another scope that may not have the same imports. + + /** This context is used to create the `TypeSplices` wrapping annotations + * below. These should have `mainFun` as their owner (and not the + * enclosing package class that we would get otherwise) so that + * subsequent owner changes (for example in `Typer.typedTypedSplice`) are + * correct. See #22364 and associated tests. */ - def insertTypeSplices = new TreeMap { - override def transform(tree: Tree)(using Context): Tree = tree match - case tree: tpd.Ident @unchecked => TypedSplice(tree) - case tree => super.transform(tree) - } + val annotsCtx = ctx.fresh.setOwner(mainFun) val annots = mainFun.annotations .filterNot(_.matches(defn.MainAnnot)) - .map(annot => insertTypeSplices.transform(annot.tree)) + .map(annot => TypedSplice(annot.tree)(using annotsCtx)) val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body) .withFlags(JavaStatic | Synthetic) .withAnnotations(annots) diff --git a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala index e77642a8e2b9..861db55b1903 100644 --- a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala +++ b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala @@ -94,7 +94,8 @@ object NavigateAST { * When choosing better fit we compare spans. If candidate span has starting or ending point inside (exclusive) * current best fit it is selected as new best fit. This means that same spans are failing the first predicate. * - * In case when spans start and end at same offsets we prefer non synthethic one. + * In case when spans start and end at same offsets we prefer non synthethic one, + * and then one with better point (see isBetterPoint below). */ def isBetterFit(currentBest: List[Positioned], candidate: List[Positioned]): Boolean = if currentBest.isEmpty && candidate.nonEmpty then true @@ -102,9 +103,20 @@ object NavigateAST { val bestSpan = currentBest.head.span val candidateSpan = candidate.head.span - bestSpan != candidateSpan && - envelops(bestSpan, candidateSpan) || - bestSpan.contains(candidateSpan) && bestSpan.isSynthetic && !candidateSpan.isSynthetic + def isBetterPoint = + // Given two spans with same end points, + // we compare their points in relation to the point we are looking for (span.point) + // The candidate (candidateSpan.point) is better than what we have so far (bestSpan.point), when: + // 1) candidate is closer to target from the right + span.point <= candidateSpan.point && candidateSpan.point < bestSpan.point + // 2) candidate is closer to target from the left + || bestSpan.point < candidateSpan.point && candidateSpan.point <= span.point + // 3) candidate is to on the left side of target, and best so far is on the right + || candidateSpan.point <= span.point && span.point < bestSpan.point + + bestSpan != candidateSpan && envelops(bestSpan, candidateSpan) + || bestSpan.contains(candidateSpan) && bestSpan.isSynthetic && !candidateSpan.isSynthetic + || candidateSpan.start == bestSpan.start && candidateSpan.end == bestSpan.end && isBetterPoint else false def isRecoveryTree(sel: untpd.Select): Boolean = @@ -141,7 +153,9 @@ object NavigateAST { case _ => val iterator = p match case defdef: DefTree[?] => - p.productIterator ++ defdef.mods.productIterator + val mods = defdef.mods + val annotations = defdef.symbol.annotations.filter(_.tree.span.contains(span)).map(_.tree) + p.productIterator ++ annotations ++ mods.productIterator case _ => p.productIterator childPath(iterator, p :: path) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 8c682971adcb..2582c1e86dc9 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -142,7 +142,7 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => def allTermArguments(tree: Tree): List[Tree] = unsplice(tree) match { case Apply(fn, args) => allTermArguments(fn) ::: args case TypeApply(fn, args) => allTermArguments(fn) - case Block(Nil, expr) => allTermArguments(expr) + case Block(_, expr) => allTermArguments(expr) case _ => Nil } @@ -150,7 +150,7 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => def allArguments(tree: Tree): List[Tree] = unsplice(tree) match { case Apply(fn, args) => allArguments(fn) ::: args case TypeApply(fn, args) => allArguments(fn) ::: args - case Block(Nil, expr) => allArguments(expr) + case Block(_, expr) => allArguments(expr) case _ => Nil } @@ -262,6 +262,19 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => case _ => false } + /** Expression was written `e: Unit` to quell warnings. Looks into adapted tree. */ + def isAscribedToUnit(tree: Tree): Boolean = + import typer.Typer.AscribedToUnit + tree.hasAttachment(AscribedToUnit) + || { + def loop(tree: Tree): Boolean = tree match + case Apply(fn, _) => fn.hasAttachment(AscribedToUnit) || loop(fn) + case TypeApply(fn, _) => fn.hasAttachment(AscribedToUnit) || loop(fn) + case Block(_, expr) => expr.hasAttachment(AscribedToUnit) || loop(expr) + case _ => false + loop(tree) + } + /** Does this CaseDef catch Throwable? */ def catchesThrowable(cdef: CaseDef)(using Context): Boolean = catchesAllOf(cdef, defn.ThrowableType) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala index 5ddbdd990182..0298c306d24d 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeTypeMap.scala @@ -224,6 +224,11 @@ class TreeTypeMap( val tmap1 = tmap.withMappedSyms( origCls(cls).typeParams ::: origDcls, cls.typeParams ::: mappedDcls) + mapped.foreach { sym => + // outer Symbols can reference nested ones in info, + // so we remap that once again with the updated TreeTypeMap + sym.info = tmap1.mapType(sym.info) + } origDcls.lazyZip(mappedDcls).foreach(cls.asClass.replace) tmap1 } diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 7652dcfc9928..9cfacf3b42fa 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -31,7 +31,7 @@ object Trees { /** Property key for backquoted identifiers and definitions */ val Backquoted: Property.StickyKey[Unit] = Property.StickyKey() - + val SyntheticUnit: Property.StickyKey[Unit] = Property.StickyKey() /** Trees take a parameter indicating what the type of their `tpe` field @@ -460,8 +460,11 @@ object Trees { else if qualifier.span.exists && qualifier.span.start > span.point then // right associative val realName = name.stripModuleClassSuffix.lastPart Span(span.start, span.start + realName.length, point) - else - Span(point, span.end, point) + else if span.pointMayBeIncorrect then + val realName = name.stripModuleClassSuffix.lastPart + val probablyPoint = span.end - realName.length + Span(probablyPoint, span.end, probablyPoint) + else Span(point, span.end, point) else span } @@ -765,6 +768,7 @@ object Trees { override def isEmpty: Boolean = !hasType override def toString: String = s"TypeTree${if (hasType) s"[$typeOpt]" else ""}" + def isInferred = false } /** Tree that replaces a level 1 splices in pickled (level 0) quotes. @@ -787,6 +791,7 @@ object Trees { */ class InferredTypeTree[+T <: Untyped](implicit @constructorOnly src: SourceFile) extends TypeTree[T]: type ThisTree[+T <: Untyped] <: InferredTypeTree[T] + override def isInferred = true /** ref.type */ case class SingletonTypeTree[+T <: Untyped] private[ast] (ref: Tree[T])(implicit @constructorOnly src: SourceFile) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index c119dfd8d982..33997c4656f5 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -477,6 +477,21 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def ref(sym: Symbol)(using Context): Tree = ref(NamedType(sym.owner.thisType, sym.name, sym.denot)) + // Like `ref`, but avoids wrapping innermost module class references with This(), + // instead mapping those to objects, so that the resulting trees can be used in + // largest scope possible (method added for macros) + def generalisedRef(sym: Symbol)(using Context): Tree = + // Removes ThisType from inner module classes, replacing those with references to objects + def simplifyThisTypePrefix(tpe: Type)(using Context): Type = + tpe match + case ThisType(tref @ TypeRef(prefix, _)) if tref.symbol.flags.is(Module) => + TermRef(simplifyThisTypePrefix(prefix), tref.symbol.companionModule) + case TypeRef(prefix, designator) => + TypeRef(simplifyThisTypePrefix(prefix), designator) + case _ => + tpe + ref(NamedType(simplifyThisTypePrefix(sym.owner.thisType), sym.name, sym.denot)) + private def followOuterLinks(t: Tree)(using Context) = t match { case t: This if ctx.erasedTypes && !(t.symbol == ctx.owner.enclosingClass || t.symbol.isStaticOwner) => // after erasure outer paths should be respected @@ -817,14 +832,6 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { Closure(tree: Tree)(env, meth, tpt) } - // This is a more fault-tolerant copier that does not cause errors when - // function types in applications are undefined. - // This was called `Inliner.InlineCopier` before 3.6.3. - class ConservativeTreeCopier() extends TypedTreeCopier: - override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(using Context): Apply = - if fun.tpe.widen.exists then super.Apply(tree)(fun, args) - else untpd.cpy.Apply(tree)(fun, args).withTypeUnchecked(tree.tpe) - override def skipTransform(tree: Tree)(using Context): Boolean = tree.tpe.isError implicit class TreeOps[ThisTree <: tpd.Tree](private val tree: ThisTree) extends AnyVal { @@ -1511,7 +1518,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { * @param selectorPredicate A test to find the selector to use. * @return The symbols imported. */ - def importedSymbols(imp: Import, + def importedSymbols(imp: ImportOrExport, selectorPredicate: untpd.ImportSelector => Boolean = util.common.alwaysTrue) (using Context): List[Symbol] = imp.selectors.find(selectorPredicate) match diff --git a/compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala b/compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala index 51b261583feb..ec04eb3db8a1 100644 --- a/compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala +++ b/compiler/src/dotty/tools/dotc/classpath/AggregateClassPath.scala @@ -51,7 +51,6 @@ case class AggregateClassPath(aggregates: Seq[ClassPath]) extends ClassPath { case (_, s) => s } } - override def asURLs: Seq[URL] = aggregates.flatMap(_.asURLs) override def asClassPathStrings: Seq[String] = aggregates.map(_.asClassPathString).distinct diff --git a/compiler/src/dotty/tools/dotc/config/CliCommand.scala b/compiler/src/dotty/tools/dotc/config/CliCommand.scala index b76af885765c..ce97e15483fd 100644 --- a/compiler/src/dotty/tools/dotc/config/CliCommand.scala +++ b/compiler/src/dotty/tools/dotc/config/CliCommand.scala @@ -7,7 +7,7 @@ import Settings.* import core.Contexts.* import printing.Highlighting -import scala.util.chaining.given +import dotty.tools.dotc.util.chaining.* import scala.PartialFunction.cond trait CliCommand: @@ -33,8 +33,8 @@ trait CliCommand: | means one or a comma-separated list of: | - (partial) phase names with an optional "+" suffix to include the next phase | - the string "all" - | example: -Xprint:all prints all phases. - | example: -Xprint:typer,mixin prints the typer and mixin phases. + | example: -Vprint:all prints all phases. + | example: -Vprint:typer,mixin prints the typer and mixin phases. | example: -Ylog:erasure+ logs the erasure phase and the phase after the erasure phase. | This is useful because during the tree transform of phase X, we often | already are in phase X + 1. diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index 884e40e97d96..7f6b5025cea6 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -27,6 +27,13 @@ class JavaPlatform extends Platform { case _ => false }) + def addToClassPath(cPath: ClassPath)(using Context): Unit = classPath match { + case AggregateClassPath(entries) => + currentClassPath = Some(AggregateClassPath(entries :+ cPath)) + case cp: ClassPath => + currentClassPath = Some(AggregateClassPath(cp :: cPath :: Nil)) + } + /** Update classpath with a substituted subentry */ def updateClassPath(subst: Map[ClassPath, ClassPath]): Unit = currentClassPath.get match { case AggregateClassPath(entries) => diff --git a/compiler/src/dotty/tools/dotc/config/PathResolver.scala b/compiler/src/dotty/tools/dotc/config/PathResolver.scala index f60727e6bba2..67be0e3587cb 100644 --- a/compiler/src/dotty/tools/dotc/config/PathResolver.scala +++ b/compiler/src/dotty/tools/dotc/config/PathResolver.scala @@ -53,8 +53,7 @@ object PathResolver { def classPathEnv: String = envOrElse("CLASSPATH", "") def sourcePathEnv: String = envOrElse("SOURCEPATH", "") - //using propOrNone/getOrElse instead of propOrElse so that searchForBootClasspath is lazy evaluated - def javaBootClassPath: String = propOrNone("sun.boot.class.path") getOrElse searchForBootClasspath + def javaBootClassPath: String = propOrElse("sun.boot.class.path", searchForBootClasspath) def javaExtDirs: String = propOrEmpty("java.ext.dirs") def scalaHome: String = propOrEmpty("scala.home") diff --git a/compiler/src/dotty/tools/dotc/config/Platform.scala b/compiler/src/dotty/tools/dotc/config/Platform.scala index db05996d98fd..798c9f449f9a 100644 --- a/compiler/src/dotty/tools/dotc/config/Platform.scala +++ b/compiler/src/dotty/tools/dotc/config/Platform.scala @@ -21,6 +21,9 @@ abstract class Platform { /** Update classpath with a substitution that maps entries to entries */ def updateClassPath(subst: Map[ClassPath, ClassPath]): Unit + /** Add new entry to classpath */ + def addToClassPath(cPath: ClassPath)(using Context): Unit + /** Any platform-specific phases. */ //def platformPhases: List[SubComponent] diff --git a/compiler/src/dotty/tools/dotc/config/Properties.scala b/compiler/src/dotty/tools/dotc/config/Properties.scala index 4ea058d2cca8..610428b0f752 100644 --- a/compiler/src/dotty/tools/dotc/config/Properties.scala +++ b/compiler/src/dotty/tools/dotc/config/Properties.scala @@ -45,7 +45,7 @@ trait PropertiesTrait { def propIsSet(name: String): Boolean = System.getProperty(name) != null def propIsSetTo(name: String, value: String): Boolean = propOrNull(name) == value - def propOrElse(name: String, alt: String): String = System.getProperty(name, alt) + def propOrElse(name: String, alt: => String): String = Option(System.getProperty(name)).getOrElse(alt) def propOrEmpty(name: String): String = propOrElse(name, "") def propOrNull(name: String): String = propOrElse(name, null) def propOrNone(name: String): Option[String] = Option(propOrNull(name)) @@ -53,11 +53,11 @@ trait PropertiesTrait { def setProp(name: String, value: String): String = System.setProperty(name, value) def clearProp(name: String): String = System.clearProperty(name) - def envOrElse(name: String, alt: String): String = Option(System getenv name) getOrElse alt + def envOrElse(name: String, alt: => String): String = Option(System getenv name) getOrElse alt def envOrNone(name: String): Option[String] = Option(System getenv name) // for values based on propFilename - def scalaPropOrElse(name: String, alt: String): String = scalaProps.getProperty(name, alt) + def scalaPropOrElse(name: String, alt: => String): String = scalaProps.getProperty(name, alt) def scalaPropOrEmpty(name: String): String = scalaPropOrElse(name, "") def scalaPropOrNone(name: String): Option[String] = Option(scalaProps.getProperty(name)) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 6928e0617069..c25e5e0db11c 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -12,7 +12,7 @@ import dotty.tools.io.{AbstractFile, Directory, JDK9Reflectors, PlainDirectory} import dotty.tools.backend.jvm.BackendUtils.classfileVersionMap import Setting.ChoiceWithHelp -import scala.util.chaining.* +import dotty.tools.dotc.util.chaining.* import java.util.zip.Deflater @@ -37,7 +37,8 @@ object ScalaSettings: ScalaRelease.values.toList.map(_.show) def supportedSourceVersions: List[String] = - SourceVersion.values.toList.map(_.toString) + SourceVersion.values.diff(SourceVersion.illegalInSettings) + .map(_.toString).toList def defaultClasspath: String = sys.env.getOrElse("CLASSPATH", ".") @@ -135,6 +136,7 @@ trait CommonScalaSettings: val usejavacp: Setting[Boolean] = BooleanSetting("-usejavacp", "Utilize the java.class.path in classpath resolution.", aliases = List("--use-java-class-path")) val scalajs: Setting[Boolean] = BooleanSetting("-scalajs", "Compile in Scala.js mode (requires scalajs-library.jar on the classpath).", aliases = List("--scalajs")) val replInitScript: Setting[String] = StringSetting("-repl-init-script", "code", "The code will be run on REPL startup.", "", aliases = List("--repl-init-script")) + val replQuitAfterInit: Setting[Boolean] = BooleanSetting("-repl-quit-after-init", "Quit REPL after evaluating the init script.", aliases = List("--repl-quit-after-init")) end CommonScalaSettings @@ -152,7 +154,7 @@ private sealed trait PluginSettings: private sealed trait VerboseSettings: self: SettingGroup => val Vhelp: Setting[Boolean] = BooleanSetting("-V", "Print a synopsis of verbose options.") - val Xprint: Setting[List[String]] = PhasesSetting("-Vprint", "Print out program after", aliases = List("-Xprint")) + val Vprint: Setting[List[String]] = PhasesSetting("-Vprint", "Print out program after", aliases = List("-Xprint")) val XshowPhases: Setting[Boolean] = BooleanSetting("-Vphases", "List compiler phases.", aliases = List("-Xshow-phases")) val Vprofile: Setting[Boolean] = BooleanSetting("-Vprofile", "Show metrics about sources and internal representations to estimate compile-time complexity.") @@ -172,6 +174,8 @@ private sealed trait WarningSettings: private val WvalueDiscard: Setting[Boolean] = BooleanSetting("-Wvalue-discard", "Warn when non-Unit expression results are unused.") private val WNonUnitStatement = BooleanSetting("-Wnonunit-statement", "Warn when block statements are non-Unit expressions.") private val WenumCommentDiscard = BooleanSetting("-Wenum-comment-discard", "Warn when a comment ambiguously assigned to multiple enum cases is discarded.") + private val WtoStringInterpolated = BooleanSetting("-Wtostring-interpolated", "Warn a standard interpolator used toString on a reference type.") + private val WrecurseWithDefault = BooleanSetting("-Wrecurse-with-default", "Warn when a method calls itself with a default argument.") private val Wunused: Setting[List[ChoiceWithHelp[String]]] = MultiChoiceHelpSetting( name = "-Wunused", helpArg = "warning", @@ -179,28 +183,21 @@ private sealed trait WarningSettings: choices = List( ChoiceWithHelp("nowarn", ""), ChoiceWithHelp("all", ""), - ChoiceWithHelp( - name = "imports", - description = "Warn if an import selector is not referenced.\n" + - "NOTE : overrided by -Wunused:strict-no-implicit-warn"), + ChoiceWithHelp("imports", "Warn if an import selector is not referenced."), ChoiceWithHelp("privates", "Warn if a private member is unused"), ChoiceWithHelp("locals", "Warn if a local definition is unused"), ChoiceWithHelp("explicits", "Warn if an explicit parameter is unused"), ChoiceWithHelp("implicits", "Warn if an implicit parameter is unused"), ChoiceWithHelp("params", "Enable -Wunused:explicits,implicits"), + ChoiceWithHelp("patvars","Warn if a variable bound in a pattern is unused"), + //ChoiceWithHelp("inlined", "Apply -Wunused to inlined expansions"), // TODO ChoiceWithHelp("linted", "Enable -Wunused:imports,privates,locals,implicits"), ChoiceWithHelp( name = "strict-no-implicit-warn", - description = "Same as -Wunused:import, only for imports of explicit named members.\n" + - "NOTE : This overrides -Wunused:imports and NOT set by -Wunused:all" + description = """Same as -Wunused:imports, only for imports of explicit named members. + |NOTE : This overrides -Wunused:imports and NOT set by -Wunused:all""".stripMargin ), - // ChoiceWithHelp("patvars","Warn if a variable bound in a pattern is unused"), - ChoiceWithHelp( - name = "unsafe-warn-patvars", - description = "(UNSAFE) Warn if a variable bound in a pattern is unused.\n" + - "This warning can generate false positive, as warning cannot be\n" + - "suppressed yet." - ) + ChoiceWithHelp("unsafe-warn-patvars", "Deprecated alias for `patvars`"), ), default = Nil ) @@ -212,7 +209,6 @@ private sealed trait WarningSettings: // Is any choice set for -Wunused? def any(using Context): Boolean = Wall.value || Wunused.value.nonEmpty - // overrided by strict-no-implicit-warn def imports(using Context) = (allOr("imports") || allOr("linted")) && !(strictNoImplicitWarn) def locals(using Context) = @@ -226,9 +222,8 @@ private sealed trait WarningSettings: def params(using Context) = allOr("params") def privates(using Context) = allOr("privates") || allOr("linted") - def patvars(using Context) = - isChoiceSet("unsafe-warn-patvars") // not with "all" - // allOr("patvars") // todo : rename once fixed + def patvars(using Context) = allOr("patvars") || isChoiceSet("unsafe-warn-patvars") + def inlined(using Context) = isChoiceSet("inlined") def linted(using Context) = allOr("linted") def strictNoImplicitWarn(using Context) = @@ -297,6 +292,8 @@ private sealed trait WarningSettings: def valueDiscard(using Context): Boolean = allOr(WvalueDiscard) def nonUnitStatement(using Context): Boolean = allOr(WNonUnitStatement) def enumCommentDiscard(using Context): Boolean = allOr(WenumCommentDiscard) + def toStringInterpolated(using Context): Boolean = allOr(WtoStringInterpolated) + def recurseWithDefault(using Context): Boolean = allOr(WrecurseWithDefault) def checkInit(using Context): Boolean = allOr(YcheckInit) /** -X "Extended" or "Advanced" settings */ diff --git a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala index b8fa7994ce0c..b59d89818483 100644 --- a/compiler/src/dotty/tools/dotc/config/SourceVersion.scala +++ b/compiler/src/dotty/tools/dotc/config/SourceVersion.scala @@ -6,10 +6,12 @@ import core.Decorators.* import util.Property enum SourceVersion: - case `3.0-migration`, `3.0`, `3.1` // Note: do not add `3.1-migration` here, 3.1 is the same language as 3.0. + case `3.0-migration`, `3.0` + case `3.1-migration`, `3.1` case `3.2-migration`, `3.2` case `3.3-migration`, `3.3` case `future-migration`, `future` + case `never` // needed for MigrationVersion.errorFrom if we never want to issue an error val isMigrating: Boolean = toString.endsWith("-migration") @@ -21,10 +23,17 @@ enum SourceVersion: def isAtMost(v: SourceVersion) = stable.ordinal <= v.ordinal object SourceVersion extends Property.Key[SourceVersion]: + def defaultSourceVersion = `3.3` + /* Illegal source versions that may not appear in the settings `-source:<...>` */ + val illegalInSettings = List(`3.1-migration`, `never`) + + /* Illegal source versions that may not appear as an import `import scala.language.<...>` */ + val illegalInImports = List(`3.1-migration`, `never`) + /** language versions that may appear in a language import, are deprecated, but not removed from the standard library. */ - val illegalSourceVersionNames = List("3.1-migration").map(_.toTermName) + val illegalSourceVersionNames = illegalInImports.map(_.toString.toTermName) /** language versions that the compiler recognises. */ val validSourceVersionNames = values.toList.map(_.toString.toTermName) diff --git a/compiler/src/dotty/tools/dotc/config/WrappedProperties.scala b/compiler/src/dotty/tools/dotc/config/WrappedProperties.scala index 20304b74c1da..a72830331e9f 100644 --- a/compiler/src/dotty/tools/dotc/config/WrappedProperties.scala +++ b/compiler/src/dotty/tools/dotc/config/WrappedProperties.scala @@ -14,12 +14,12 @@ trait WrappedProperties extends PropertiesTrait { protected def propCategory: String = "wrapped" protected def pickJarBasedOn: Class[?] = this.getClass - override def propIsSet(name: String): Boolean = wrap(super.propIsSet(name)) exists (x => x) - override def propOrElse(name: String, alt: String): String = wrap(super.propOrElse(name, alt)) getOrElse alt - override def setProp(name: String, value: String): String = wrap(super.setProp(name, value)).orNull - override def clearProp(name: String): String = wrap(super.clearProp(name)).orNull - override def envOrElse(name: String, alt: String): String = wrap(super.envOrElse(name, alt)) getOrElse alt - override def envOrNone(name: String): Option[String] = wrap(super.envOrNone(name)).flatten + override def propIsSet(name: String): Boolean = wrap(super.propIsSet(name)) exists (x => x) + override def propOrElse(name: String, alt: => String): String = wrap(super.propOrElse(name, alt)) getOrElse alt + override def setProp(name: String, value: String): String = wrap(super.setProp(name, value)).orNull + override def clearProp(name: String): String = wrap(super.clearProp(name)).orNull + override def envOrElse(name: String, alt: => String): String = wrap(super.envOrElse(name, alt)) getOrElse alt + override def envOrNone(name: String): Option[String] = wrap(super.envOrNone(name)).flatten def systemProperties: Iterator[(String, String)] = { import scala.jdk.CollectionConverters.* diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index f5825df84a8b..0706003f533e 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -65,7 +65,7 @@ object Annotations { if tm.isRange(x) then x else val tp1 = tm(tree.tpe) - foldOver(if !tp1.exists || (tp1 frozen_=:= tree.tpe) then x else tp1, tree) + foldOver(if !tp1.exists || tp1.eql(tree.tpe) then x else tp1, tree) val diff = findDiff(NoType, args) if tm.isRange(diff) then EmptyAnnotation else if diff.exists then derivedAnnotation(tm.mapOver(tree)) diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 0ceaaac75063..617104630850 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -660,11 +660,20 @@ trait ConstraintHandling { def widenOr(tp: Type) = if widenUnions then val tpw = tp.widenUnion - if (tpw ne tp) && !isTransparent(tpw, traitOnly = false) && (tpw <:< bound) then tpw else tp + if tpw ne tp then + if tpw.isTransparent() then + // Now also widen singletons of soft unions. Before these were skipped + // since widenUnion on soft unions is independent of whether singletons + // are widened or not. This avoids an expensive subtype check in widenSingle, + // see i19907_*.scala for test cases. + widenSingle(tp, skipSoftUnions = false) + else if tpw <:< bound then tpw + else tp + else tp else tp.hardenUnions - def widenSingle(tp: Type) = - val tpw = tp.widenSingletons + def widenSingle(tp: Type, skipSoftUnions: Boolean) = + val tpw = tp.widenSingletons(skipSoftUnions) if (tpw ne tp) && (tpw <:< bound) then tpw else tp def isSingleton(tp: Type): Boolean = tp match @@ -674,7 +683,7 @@ trait ConstraintHandling { val wideInst = if isSingleton(bound) then inst else - val widenedFromSingle = widenSingle(inst) + val widenedFromSingle = widenSingle(inst, skipSoftUnions = widenUnions) val widenedFromUnion = widenOr(widenedFromSingle) val widened = dropTransparentTraits(widenedFromUnion, bound) widenIrreducible(widened) diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index bde23de135e4..8c09a3f24fa0 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -121,7 +121,7 @@ object Contexts { * risk of capturing complete trees. * - To make sure these rules are kept, it would be good to do a sanity * check using bytecode inspection with javap or scalap: Keep track - * of all class fields of type context; allow them only in whitelisted + * of all class fields of type context; allow them only in allowlisted * classes (which should be short-lived). */ abstract class Context(val base: ContextBase) { thiscontext => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4aed3d310b03..623d9e8c77ca 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -482,6 +482,8 @@ class Definitions { @tu lazy val Predef_undefined: Symbol = ScalaPredefModule.requiredMethod(nme.???) @tu lazy val ScalaPredefModuleClass: ClassSymbol = ScalaPredefModule.moduleClass.asClass + @tu lazy val SameTypeClass: ClassSymbol = requiredClass("scala.=:=") + @tu lazy val SameType_refl: Symbol = SameTypeClass.companionModule.requiredMethod(nme.refl) @tu lazy val SubTypeClass: ClassSymbol = requiredClass("scala.<:<") @tu lazy val SubType_refl: Symbol = SubTypeClass.companionModule.requiredMethod(nme.refl) @@ -494,6 +496,9 @@ class Definitions { @tu lazy val ScalaRuntime_toArray: Symbol = ScalaRuntimeModule.requiredMethod(nme.toArray) @tu lazy val ScalaRuntime_toObjectArray: Symbol = ScalaRuntimeModule.requiredMethod(nme.toObjectArray) + @tu lazy val MurmurHash3Module: Symbol = requiredModule("scala.util.hashing.MurmurHash3") + @tu lazy val MurmurHash3_productHash = MurmurHash3Module.info.member(termName("productHash")).suchThat(_.info.firstParamTypes.size == 3).symbol + @tu lazy val BoxesRunTimeModule: Symbol = requiredModule("scala.runtime.BoxesRunTime") @tu lazy val BoxesRunTimeModule_externalEquals: Symbol = BoxesRunTimeModule.info.decl(nme.equals_).suchThat(toDenot(_).info.firstParamTypes.size == 2).symbol @tu lazy val ScalaStaticsModule: Symbol = requiredModule("scala.runtime.Statics") @@ -809,6 +814,7 @@ class Definitions { @tu lazy val ReflectSelectableTypeRef: TypeRef = requiredClassRef("scala.reflect.Selectable") + @tu lazy val TypeableType: TypeSymbol = requiredPackage("scala.reflect.Typeable$package").moduleClass.requiredType("Typeable") @tu lazy val TypeTestClass: ClassSymbol = requiredClass("scala.reflect.TypeTest") @tu lazy val TypeTest_unapply: Symbol = TypeTestClass.requiredMethod(nme.unapply) @tu lazy val TypeTestModule_identity: Symbol = TypeTestClass.companionModule.requiredMethod(nme.identity) @@ -816,6 +822,7 @@ class Definitions { @tu lazy val QuotedExprClass: ClassSymbol = requiredClass("scala.quoted.Expr") @tu lazy val QuotesClass: ClassSymbol = requiredClass("scala.quoted.Quotes") + @tu lazy val Quotes_reflectModule: Symbol = QuotesClass.requiredClass("reflectModule") @tu lazy val Quotes_reflect: Symbol = QuotesClass.requiredValue("reflect") @tu lazy val Quotes_reflect_asTerm: Symbol = Quotes_reflect.requiredMethod("asTerm") @tu lazy val Quotes_reflect_Apply: Symbol = Quotes_reflect.requiredValue("Apply") @@ -941,6 +948,7 @@ class Definitions { def NonEmptyTupleClass(using Context): ClassSymbol = NonEmptyTupleTypeRef.symbol.asClass lazy val NonEmptyTuple_tail: Symbol = NonEmptyTupleClass.requiredMethod("tail") @tu lazy val PairClass: ClassSymbol = requiredClass("scala.*:") + @tu lazy val PairClass_unapply: Symbol = PairClass.companionModule.requiredMethod("unapply") @tu lazy val TupleXXLClass: ClassSymbol = requiredClass("scala.runtime.TupleXXL") def TupleXXLModule(using Context): Symbol = TupleXXLClass.companionModule @@ -1031,6 +1039,7 @@ class Definitions { @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val WithPureFunsAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WithPureFuns") + @tu lazy val LanguageFeatureMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.languageFeature") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") @tu lazy val BeanSetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanSetter") @tu lazy val FieldMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.field") @@ -1717,18 +1726,30 @@ class Definitions { def isPolymorphicAfterErasure(sym: Symbol): Boolean = (sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf) || (sym eq Object_synchronized) - /** Is this type a `TupleN` type? + def isTypeTestOrCast(sym: Symbol): Boolean = + (sym eq Any_isInstanceOf) + || (sym eq Any_asInstanceOf) + || (sym eq Any_typeTest) + || (sym eq Any_typeCast) + + /** Is `tp` a `TupleN` type? + * + * @return true if the type of `tp` is `TupleN[T1, T2, ..., Tn]` + */ + def isDirectTupleNType(tp: Type)(using Context): Boolean = + val arity = tp.argInfos.length + arity <= MaxTupleArity && { + val tupletp = TupleType(arity) + tupletp != null && tp.isRef(tupletp.symbol) + } + + /** Is `tp` (an alias of) a `TupleN` type? * * @return true if the dealiased type of `tp` is `TupleN[T1, T2, ..., Tn]` */ - def isTupleNType(tp: Type)(using Context): Boolean = { + def isTupleNType(tp: Type)(using Context): Boolean = val tp1 = tp.dealias - val arity = tp1.argInfos.length - arity <= MaxTupleArity && { - val tupletp = TupleType(arity) - tupletp != null && tp1.isRef(tupletp.symbol) - } - } + isDirectTupleNType(tp1) def tupleType(elems: List[Type]): Type = { val arity = elems.length diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index 84ef94687b13..fdf6bf265096 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -1299,9 +1299,9 @@ object Denotations { } /** The current denotation of the static reference given by path, - * or a MissingRef or NoQualifyingRef instance, if it does not exist. - * if generateStubs is set, generates stubs for missing top-level symbols - */ + * or a MissingRef or NoQualifyingRef instance, if it does not exist. + * if generateStubs is set, generates stubs for missing top-level symbols + */ def staticRef(path: Name, generateStubs: Boolean = true, isPackage: Boolean = false)(using Context): Denotation = { def select(prefix: Denotation, selector: Name): Denotation = { val owner = prefix.disambiguate(_.info.isParameterless) diff --git a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala index 44ce31b27a73..d685b3253f59 100644 --- a/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/GadtConstraint.scala @@ -33,6 +33,18 @@ class GadtConstraint private ( reverseMapping = reverseMapping.updated(tv.origin, sym), ) + def replace(param: TypeParamRef, tp: Type)(using Context) = + var constr = constraint + for + poly <- constraint.domainLambdas + paramRef <- poly.paramRefs + do + val entry0 = constr.entry(paramRef) + val entry1 = entry0.substParam(param, tp) + if entry1 ne entry0 then + constr = constr.updateEntry(paramRef, entry1) + withConstraint(constr) + /** Is `sym1` ordered to be less than `sym2`? */ def isLess(sym1: Symbol, sym2: Symbol)(using Context): Boolean = constraint.isLess(tvarOrError(sym1).origin, tvarOrError(sym2).origin) @@ -241,6 +253,9 @@ sealed trait GadtState { result } + def replace(param: TypeParamRef, tp: Type)(using Context) = + gadt = gadt.replace(param, tp) + /** See [[ConstraintHandling.approximation]] */ def approximation(sym: Symbol, fromBelow: Boolean, maxLevel: Int = Int.MaxValue)(using Context): Type = { approximation(gadt.tvarOrError(sym).origin, fromBelow, maxLevel).match diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index e4364d168267..91a5dfde1c47 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -9,6 +9,25 @@ import TypeApplications.EtaExpansion /** Operations that are shared between Namer and TreeUnpickler */ object NamerOps: + /** A completer supporting cleanup actions. + * Needed to break the loop between completion of class and companion object. + * If we try to complete the class first, and completion needs the companion + * object (for instance for processing an import) then the companion object + * completion would consult the companion class info for constructor that + * need a constructor proxy in the object. This can lead to a cyclic reference. + * We break the cycle by delaying adding constructor proxies to be a cleanuo + * action instead. + */ + trait CompleterWithCleanup extends LazyType: + private var cleanupActions: List[() => Unit] = Nil + def addCleanupAction(op: () => Unit): Unit = + cleanupActions = op :: cleanupActions + def cleanup(): Unit = + if cleanupActions.nonEmpty then + cleanupActions.reverse.foreach(_()) + cleanupActions = Nil + end CompleterWithCleanup + /** The type of the constructed instance is returned * * @param ctor the constructor @@ -110,11 +129,22 @@ object NamerOps: */ def addConstructorApplies(scope: MutableScope, cls: ClassSymbol, modcls: ClassSymbol)(using Context): scope.type = def proxy(constr: Symbol): Symbol = + var flags = ApplyProxyFlags | (constr.flagsUNSAFE & AccessFlags) + if cls.is(Protected) && !modcls.is(Protected) then flags |= Protected newSymbol( - modcls, nme.apply, ApplyProxyFlags | (constr.flagsUNSAFE & AccessFlags), - ApplyProxyCompleter(constr), coord = constr.coord) - for dcl <- cls.info.decls do + modcls, nme.apply, + flags, + ApplyProxyCompleter(constr), + cls.privateWithin, + constr.coord) + def doAdd() = for dcl <- cls.info.decls do if dcl.isConstructor then scope.enter(proxy(dcl)) + cls.infoOrCompleter match + case completer: CompleterWithCleanup if cls.is(Touched) => + // Taking the info would lead to a cyclic reference here - delay instead until cleanup of `cls` + completer.addCleanupAction(doAdd) + case _ => + doAdd() scope end addConstructorApplies @@ -133,10 +163,13 @@ object NamerOps: /** A new symbol that is the constructor companion for class `cls` */ def classConstructorCompanion(cls: ClassSymbol)(using Context): TermSymbol = + var flags = ConstructorCompanionFlags + if cls.is(Protected) then flags |= Protected val companion = newModuleSymbol( cls.owner, cls.name.toTermName, - ConstructorCompanionFlags, ConstructorCompanionFlags, + flags, flags, constructorCompanionCompleter(cls), + cls.privateWithin, coord = cls.coord, assocFile = cls.assocFile) companion.moduleClass.registerCompanion(cls) diff --git a/compiler/src/dotty/tools/dotc/core/Names.scala b/compiler/src/dotty/tools/dotc/core/Names.scala index 3f9667b08067..a31ab0662ee4 100644 --- a/compiler/src/dotty/tools/dotc/core/Names.scala +++ b/compiler/src/dotty/tools/dotc/core/Names.scala @@ -401,7 +401,7 @@ object Names { } /** It's OK to take a toString if the stacktrace does not contain a method - * from GenBCode or it also contains one of the whitelisted methods below. + * from GenBCode or it also contains one of the allowed methods below. */ private def toStringOK = { val trace: Array[StackTraceElement] = Thread.currentThread.nn.getStackTrace.asInstanceOf[Array[StackTraceElement]] diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index c9b60ac051ad..78c717539763 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -263,9 +263,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds, private var coDeps: ReverseDeps = SimpleIdentityMap.empty /** A map that associates type parameters of this constraint with all other type - * parameters that refer to them in their bounds covariantly, such that, if the + * parameters that refer to them in their bounds contravariantly, such that, if the * type parameter is instantiated to a smaller type, the constraint would be narrowed. - * (i.e. solution set changes other than simply being made larger). + * (i.e. solution set changes other than simply being made smaller). */ private var contraDeps: ReverseDeps = SimpleIdentityMap.empty @@ -368,7 +368,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, /** Adjust reverse dependencies of all type parameters referenced by `bound` * @param isLower `bound` is a lower bound - * @param add if true, add referenced variables to dependencoes, otherwise drop them. + * @param add if true, add referenced variables to dependencies, otherwise drop them. */ def adjustReferenced(bound: Type, isLower: Boolean, add: Boolean) = adjuster.variance = if isLower then 1 else -1 @@ -394,8 +394,8 @@ class OrderingConstraint(private val boundsMap: ParamBounds, } case _ => false - /** Add or remove depenencies referenced in `bounds`. - * @param add if true, dependecies are added, otherwise they are removed + /** Add or remove dependencies referenced in `bounds`. + * @param add if true, dependencies are added, otherwise they are removed */ def adjustBounds(bounds: TypeBounds, add: Boolean) = adjustReferenced(bounds.lo, isLower = true, add) diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 8d690a28a403..dba638ca13d6 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -300,7 +300,7 @@ object Phases { * instance, it is possible to print trees after a given phase using: * * ```bash - * $ ./bin/scalac -Xprint: sourceFile.scala + * $ ./bin/scalac -Vprint: sourceFile.scala * ``` */ def phaseName: String @@ -462,15 +462,14 @@ object Phases { * Enrich crash messages. */ final def monitor(doing: String)(body: Context ?=> Unit)(using Context): Boolean = - val unit = ctx.compilationUnit - if ctx.run.enterUnit(unit) then + ctx.run.enterUnit(ctx.compilationUnit) + && { try {body; true} catch case NonFatal(ex) if !ctx.run.enrichedErrorMessage => - report.echo(ctx.run.enrichErrorMessage(s"exception occurred while $doing $unit")) + report.echo(ctx.run.enrichErrorMessage(s"exception occurred while $doing ${ctx.compilationUnit}")) throw ex finally ctx.run.advanceUnit() - else - false + } inline def runSubPhase[T](id: Run.SubPhase)(inline body: (Run.SubPhase, Context) ?=> T)(using Context): T = given Run.SubPhase = id diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index c26d8c320897..62444c175e1e 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -684,7 +684,7 @@ object SymDenotations { isAbstractOrAliasType && !isAbstractOrParamType /** Is this symbol an abstract or alias type? */ - final def isAbstractOrAliasType: Boolean = isType & !isClass + final def isAbstractOrAliasType: Boolean = isType && !isClass /** Is this symbol an abstract type or type parameter? */ final def isAbstractOrParamType(using Context): Boolean = this.isOneOf(DeferredOrTypeParam) @@ -1218,6 +1218,15 @@ object SymDenotations { else if (this.exists) owner.enclosingMethod else NoSymbol + /** The closest enclosing method or static symbol containing this definition. + * A local dummy owner is mapped to the primary constructor of the class. + */ + final def enclosingMethodOrStatic(using Context): Symbol = + if this.is(Method) || this.hasAnnotation(defn.ScalaStaticAnnot) then symbol + else if this.isClass then primaryConstructor + else if this.exists then owner.enclosingMethodOrStatic + else NoSymbol + /** The closest enclosing extension method containing this definition, * including methods outside the current class. */ @@ -1923,7 +1932,7 @@ object SymDenotations { case _ => NoSymbol /** The explicitly given self type (self types of modules are assumed to be - * explcitly given here). + * explicitly given here). */ def givenSelfType(using Context): Type = classInfo.selfInfo match { case tp: Type => tp @@ -1936,7 +1945,6 @@ object SymDenotations { /** The this-type depends on the kind of class: * - for a package class `p`: ThisType(TypeRef(Noprefix, p)) - * - for a module class `m`: A term ref to m's source module. * - for all other classes `c` with owner `o`: ThisType(TypeRef(o.thisType, c)) */ override def thisType(using Context): Type = { @@ -2674,7 +2682,7 @@ object SymDenotations { || owner.isRefinementClass || owner.is(Scala2x) || owner.unforcedDecls.contains(denot.name, denot.symbol) - || (denot.is(Synthetic) && denot.is(ModuleClass) && stillValidInOwner(denot.companionClass)) + || (denot.is(Synthetic) && denot.is(ModuleClass) && denot.companionClass.exists && stillValidInOwner(denot.companionClass)) || denot.isSelfSym || denot.isLocalDummy) catch case ex: StaleSymbol => false diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index a0b3460d06e4..bcb61ca47b3e 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -7,6 +7,7 @@ import java.nio.channels.ClosedByInterruptException import scala.util.control.NonFatal +import dotty.tools.dotc.classpath.PackageNameUtils import dotty.tools.io.{ ClassPath, ClassRepresentation, AbstractFile, NoAbstractFile } import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions @@ -265,7 +266,7 @@ object SymbolLoaders { def maybeModuleClass(classRep: ClassRepresentation): Boolean = classRep.name.last == '$' - private def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = { + def enterClasses(root: SymDenotation, packageName: String, flat: Boolean)(using Context) = { def isAbsent(classRep: ClassRepresentation) = !root.unforcedDecls.lookup(classRep.name.toTypeName).exists @@ -309,6 +310,32 @@ object SymbolLoaders { } } } + + def mergeNewEntries( + packageClass: ClassSymbol, fullPackageName: String, + jarClasspath: ClassPath, fullClasspath: ClassPath, + )(using Context): Unit = + if jarClasspath.classes(fullPackageName).nonEmpty then + // if the package contains classes in jarClasspath, the package is invalidated (or removed if there are no more classes in it) + val packageVal = packageClass.sourceModule.asInstanceOf[TermSymbol] + if packageClass.isRoot then + val loader = new PackageLoader(packageVal, fullClasspath) + loader.enterClasses(defn.EmptyPackageClass, fullPackageName, flat = false) + loader.enterClasses(defn.EmptyPackageClass, fullPackageName, flat = true) + else if packageClass.ownersIterator.contains(defn.ScalaPackageClass) then + () // skip + else if fullClasspath.hasPackage(fullPackageName) then + packageClass.info = new PackageLoader(packageVal, fullClasspath) + else + packageClass.owner.info.decls.openForMutations.unlink(packageVal) + else + for p <- jarClasspath.packages(fullPackageName) do + val subPackageName = PackageNameUtils.separatePkgAndClassNames(p.name)._2.toTermName + val subPackage = packageClass.info.decl(subPackageName).orElse: + // package does not exist in symbol table, create a new symbol + enterPackage(packageClass, subPackageName, (module, modcls) => new PackageLoader(module, fullClasspath)) + mergeNewEntries(subPackage.asSymDenotation.moduleClass.asClass, p.name, jarClasspath, fullClasspath) + end mergeNewEntries } /** A lazy type that completes itself by calling parameter doComplete. diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index 893392794a51..e2cc098398e9 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -323,13 +323,25 @@ object Symbols extends SymUtils { targets.match case (tp: NamedType) :: _ => tp.symbol.sourceSymbol case _ => this - else if (denot.is(Synthetic)) { + else if denot.is(Synthetic) then val linked = denot.linkedClass if (linked.exists && !linked.is(Synthetic)) linked else denot.owner.sourceSymbol - } + else if ( + denot.is(TypeParam) && + denot.maybeOwner.maybeOwner.isAllOf(EnumCase) && + denot.maybeOwner.isPrimaryConstructor + ) then + val enclosingEnumCase = denot.maybeOwner.maybeOwner + val caseTypeParam = enclosingEnumCase.typeParams.find(_.name == denot.name) + if caseTypeParam.exists(_.is(Synthetic)) then + val enumClass = enclosingEnumCase.info.firstParent.typeSymbol + val sourceTypeParam = enumClass.typeParams.find(_.name == denot.name) + sourceTypeParam.getOrElse(this) + else + caseTypeParam.getOrElse(this) else if (denot.isPrimaryConstructor) denot.owner.sourceSymbol else this diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index ab216bfd147d..64b9a1f082ba 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -310,6 +310,34 @@ class TypeApplications(val self: Type) extends AnyVal { def ensureLambdaSub(using Context): Type = if (isLambdaSub) self else EtaExpansion(self) + + /** Convert a type constructor `TC` which has type parameters `X1, ..., Xn` + * to `[X1, ..., Xn] -> TC[X1, ..., Xn]`. + */ + def etaExpand(using Context): Type = + val tparams = self.typeParams + val resType = self.appliedTo(tparams.map(_.paramRef)) + self.dealias match + case self: TypeRef if tparams.nonEmpty && self.symbol.isClass => + val owner = self.symbol.owner + // Calling asSeenFrom on the type parameter infos is important + // so that class type references within another prefix have + // their type parameters' info fixed. + // e.g. from pos/i18569: + // trait M1: + // trait A + // trait F[T <: A] + // object M2 extends M1 + // Type parameter T in M1.F has an upper bound of M1#A + // But eta-expanding M2.F should have type parameters with an upper-bound of M2.A. + // So we take the prefix M2.type and the F symbol's owner, M1, + // to call asSeenFrom on T's info. + HKTypeLambda(tparams.map(_.paramName))( + tl => tparams.map(p => HKTypeLambda.toPInfo(tl.integrate(tparams, p.paramInfo.asSeenFrom(self.prefix, owner)))), + tl => tl.integrate(tparams, resType)) + case _ => + HKTypeLambda.fromParams(tparams, resType) + /** Eta expand if `self` is a (non-lambda) class reference and `bound` is a higher-kinded type */ def EtaExpandIfHK(bound: Type)(using Context): Type = { val hkParams = bound.hkTypeParams diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index c69ea177ee96..f9f8a4d6618f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -390,7 +390,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling compareErasedValueType case ConstantType(v2) => tp1 match { - case ConstantType(v1) => v1.value == v2.value && recur(v1.tpe, v2.tpe) + case ConstantType(v1) => v1 == v2 && recur(v1.tpe, v2.tpe) case _ => secondTry } case tp2: AnyConstantType => @@ -479,7 +479,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def widenOK = (tp2.widenSingletons eq tp2) && (tp1.widenSingletons ne tp1) - && inFrozenGadtAndConstraint(recur(tp1.widenSingletons, tp2)) + && inFrozenGadtAndConstraint(recur(tp1.widenSingletons(), tp2)) def joinOK = tp2.dealiasKeepRefiningAnnots match { case tp2: AppliedType if !tp2.tycon.typeSymbol.isClass => @@ -2772,11 +2772,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * * 1. Single inheritance of classes * 2. Final classes cannot be extended - * 3. ConstantTypes with distinct values are non intersecting - * 4. TermRefs with distinct values are non intersecting + * 3. ConstantTypes with distinct values are non-intersecting + * 4. TermRefs with distinct values are non-intersecting * 5. There is no value of type Nothing * - * Note on soundness: the correctness of match types relies on on the + * Note on soundness: the correctness of match types relies on the * property that in all possible contexts, the same match type expression * is either stuck or reduces to the same case. */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 17ddc93a10f2..747f79228e36 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -327,7 +327,7 @@ object TypeErasure { val sym = t.symbol // Only a few classes have both primitives and references as subclasses. if (sym eq defn.AnyClass) || (sym eq defn.AnyValClass) || (sym eq defn.MatchableClass) || (sym eq defn.SingletonClass) - || isScala2 && !(t.derivesFrom(defn.ObjectClass) || t.isNullType) then + || isScala2 && !(t.derivesFrom(defn.ObjectClass) || t.isNullType | t.isNothingType) then NoSymbol // We only need to check for primitives because derived value classes in arrays are always boxed. else if sym.isPrimitiveValueClass then @@ -365,6 +365,8 @@ object TypeErasure { case tp: MatchType => val alts = tp.alternatives alts.nonEmpty && !fitsInJVMArray(alts.reduce(OrType(_, _, soft = true))) + case tp @ AppliedType(tycon, _) if tycon.isLambdaSub => + !fitsInJVMArray(tp.translucentSuperType) case tp: TypeProxy => isGenericArrayElement(tp.translucentSuperType, isScala2) case tp: AndType => @@ -597,8 +599,8 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * will be returned. * * In all other situations, |T| will be computed as follow: - * - For a refined type scala.Array+[T]: - * - if T is Nothing or Null, []Object + * - For a refined type scala.Array[T]: + * - {Scala 2} if T is Nothing or Null, []Object * - otherwise, if T <: Object, []|T| * - otherwise, if T is a type parameter coming from Java, []Object * - otherwise, Object @@ -779,12 +781,14 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst private def eraseArray(tp: Type)(using Context) = { val defn.ArrayOf(elemtp) = tp: @unchecked - if (isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2)) defn.ObjectType + if isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2) then + defn.ObjectType + else if sourceLanguage.isScala2 && (elemtp.hiBound.isNullType || elemtp.hiBound.isNothingType) then + JavaArrayType(defn.ObjectType) else - try - val eElem = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp) - if eElem.isInstanceOf[WildcardType] then WildcardType - else JavaArrayType(eElem) + try erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp) match + case _: WildcardType => WildcardType + case elem => JavaArrayType(elem) catch case ex: Throwable => handleRecursive("erase array type", tp.show, ex) } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 8f87eddb6cef..c608afeaca79 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -123,6 +123,8 @@ object TypeOps: } def isLegalPrefix(pre: Type)(using Context): Boolean = + // isLegalPrefix is relaxed after typer unless we're doing an implicit + // search (this matters when doing summonInline in an inline def like in tests/pos/i17222.8.scala). pre.isStable || !ctx.phase.isTyper /** Implementation of Types#simplified */ @@ -284,7 +286,15 @@ object TypeOps: } case AndType(tp11, tp12) => mergeRefinedOrApplied(tp11, tp2) & mergeRefinedOrApplied(tp12, tp2) - case tp1: TypeParamRef if tp1 == tp2 => tp1 + case tp1: TypeParamRef => + tp2.stripTypeVar match + case tp2: TypeParamRef if tp1 == tp2 => tp1 + case _ => fail + case tp1: TypeVar => + tp2 match + case tp2: TypeVar if tp1 == tp2 => tp1 + case tp2: TypeParamRef if tp1.stripTypeVar == tp2 => tp2 + case _ => fail case _ => fail } } diff --git a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala index 41d6bf1f4fae..8f462569e39b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeUtils.scala @@ -60,7 +60,7 @@ class TypeUtils: def tupleElementTypesUpTo(bound: Int, normalize: Boolean = true)(using Context): Option[List[Type]] = def recur(tp: Type, bound: Int): Option[List[Type]] = if bound < 0 then Some(Nil) - else (if normalize then tp.normalized else tp).dealias match + else (if normalize then tp.dealias.normalized else tp).dealias match case AppliedType(tycon, hd :: tl :: Nil) if tycon.isRef(defn.PairClass) => recur(tl, bound - 1).map(hd :: _) case tp: AppliedType if defn.isTupleNType(tp) && normalize => @@ -115,6 +115,16 @@ class TypeUtils: case Some(types) => TypeOps.nestedPairs(types) case None => throw new AssertionError("not a tuple") + /** If this is a generic tuple type with arity <= MaxTupleArity, return the + * corresponding TupleN type, otherwise return this. + */ + def normalizedTupleType(using Context): Type = + if self.isGenericTuple then + self.tupleElementTypes match + case Some(elems) if elems.size <= Definitions.MaxTupleArity => defn.tupleType(elems) + case _ => self + else + self def refinedWith(name: Name, info: Type)(using Context) = RefinedType(self, name, info) /** Is this type a methodic type that takes at least one parameter? */ @@ -127,6 +137,16 @@ class TypeUtils: case mt: MethodType => mt.isImplicitMethod || mt.resType.takesImplicitParams case _ => false + /** Is this a type deriving only from transparent classes? + * @param traitOnly if true, all class symbols must be transparent traits + */ + def isTransparent(traitOnly: Boolean = false)(using Context): Boolean = self match + case AndType(tp1, tp2) => + tp1.isTransparent(traitOnly) && tp2.isTransparent(traitOnly) + case _ => + val cls = self.underlyingClassRef(refinementOK = false).typeSymbol + cls.isTransparentClass && (!traitOnly || cls.is(Trait)) + /** The constructors of this type that are applicable to `argTypes`, without needing * an implicit conversion. Curried constructors are always excluded. * @param adaptVarargs if true, allow a constructor with just a varargs argument to diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 6cfe58bae2ea..f0ca272cce36 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -338,6 +338,23 @@ object Types extends TypeUtils { case AndType(tpL, tpR) => tpL.isEffectivelySingleton || tpR.isEffectivelySingleton case _ => false + /** Is this upper-bounded by a (possibly aliased) singleton type? + * Overridden in TypeVar + */ + def isSingletonBounded(frozen: Boolean)(using Context): Boolean = this.dealias.normalized match + case tp: SingletonType => tp.isStable + case tp: TypeParamRef => + ctx.typerState.constraint.bounds(tp).hi.isSingletonBounded(frozen) + case tp: TypeRef => + tp.name == tpnme.Singleton && tp.symbol == defn.SingletonClass + || tp.superType.isSingletonBounded(frozen) + case tp: TypeVar if !tp.isInstantiated => + if frozen then tp frozen_<:< defn.SingletonType else tp <:< defn.SingletonType + case tp: HKTypeLambda => false + case tp: TypeProxy => tp.superType.isSingletonBounded(frozen) + case tp: AndType => tp.tp1.isSingletonBounded(frozen) || tp.tp2.isSingletonBounded(frozen) + case _ => false + /** Is this type of kind `AnyKind`? */ def hasAnyKind(using Context): Boolean = { @tailrec def loop(tp: Type): Boolean = tp match { @@ -392,10 +409,9 @@ object Types extends TypeUtils { case _ => false /** Does the type carry an annotation that is an instance of `cls`? */ - @tailrec final def hasAnnotation(cls: ClassSymbol)(using Context): Boolean = stripTypeVar match { - case AnnotatedType(tp, annot) => (annot matches cls) || (tp hasAnnotation cls) + @tailrec final def hasAnnotation(cls: ClassSymbol)(using Context): Boolean = stripTypeVar match + case AnnotatedType(tp, annot) => annot.matches(cls) || tp.hasAnnotation(cls) case _ => false - } /** Does this type have a supertype with an annotation satisfying given predicate `p`? */ def derivesAnnotWith(p: Annotation => Boolean)(using Context): Boolean = this match { @@ -556,8 +572,8 @@ object Types extends TypeUtils { case AndType(l, r) => val lsym = l.classSymbol val rsym = r.classSymbol - if (lsym isSubClass rsym) lsym - else if (rsym isSubClass lsym) rsym + if lsym.isSubClass(rsym) then lsym + else if rsym.isSubClass(lsym) then rsym else NoSymbol case tp: OrType => if tp.tp1.hasClassSymbol(defn.NothingClass) then @@ -696,7 +712,7 @@ object Types extends TypeUtils { case tp: TypeProxy => tp.superType.findDecl(name, excluded) case err: ErrorType => - newErrorSymbol(classSymbol orElse defn.RootClass, name, err.msg) + newErrorSymbol(classSymbol.orElse(defn.RootClass), name, err.msg) case _ => NoDenotation } @@ -776,8 +792,10 @@ object Types extends TypeUtils { goOr(tp) case tp: JavaArrayType => defn.ObjectType.findMember(name, pre, required, excluded) + case tp: WildcardType => + go(tp.bounds) case err: ErrorType => - newErrorSymbol(pre.classSymbol orElse defn.RootClass, name, err.msg) + newErrorSymbol(pre.classSymbol.orElse(defn.RootClass), name, err.msg) case _ => NoDenotation } @@ -872,7 +890,7 @@ object Types extends TypeUtils { // member in Super instead of Sub. // As an example of this in the wild, see // loadClassWithPrivateInnerAndSubSelf in ShowClassTests - go(tp.cls.typeRef) orElse d + go(tp.cls.typeRef).orElse(d) def goParam(tp: TypeParamRef) = { val next = tp.underlying @@ -966,6 +984,23 @@ object Types extends TypeUtils { buf.toList } + /** For use in quotes reflect. + * A bit slower than the usual approach due to the use of LinkedHashSet. + **/ + def sortedParents(using Context): mutable.LinkedHashSet[Type] = this match + case tp: ClassInfo => + mutable.LinkedHashSet(tp) | mutable.LinkedHashSet(tp.declaredParents.flatMap(_.sortedParents.toList)*) + case tp: RefinedType => + tp.parent.sortedParents + case tp: TypeProxy => + tp.superType.sortedParents + case tp: AndType => + tp.tp1.sortedParents | tp.tp2.sortedParents + case tp: OrType => + tp.tp1.sortedParents & tp.tp2.sortedParents + case _ => + mutable.LinkedHashSet() + /** The set of abstract term members of this type. */ final def abstractTermMembers(using Context): Seq[SingleDenotation] = { record("abstractTermMembers") @@ -1110,7 +1145,7 @@ object Types extends TypeUtils { false def relaxed_<:<(that: Type)(using Context): Boolean = - (this <:< that) || (this isValueSubType that) + (this <:< that) || this.isValueSubType(that) /** Is this type a legal type for member `sym1` that overrides another * member `sym2` of type `that`? This is the same as `<:<`, except that @@ -1164,10 +1199,10 @@ object Types extends TypeUtils { * vice versa. */ def matchesLoosely(that: Type)(using Context): Boolean = - (this matches that) || { + this.matches(that) || { val thisResult = this.widenExpr val thatResult = that.widenExpr - (this eq thisResult) != (that eq thatResult) && (thisResult matchesLoosely thatResult) + (this eq thisResult) != (that eq thatResult) && thisResult.matchesLoosely(thatResult) } /** The basetype of this type with given class symbol, NoType if `base` is not a class. */ @@ -1361,19 +1396,19 @@ object Types extends TypeUtils { case tp => tp - /** Widen all top-level singletons reachable by dealiasing - * and going to the operands of & and |. - * Overridden and cached in OrType. + /** Widen all top-level singletons reachable by dealiasing and going to the + * operands of intersections and soft unions (only when `skipSoftUnions` is + * `false`). Overridden and cached in [[OrType]]. */ - def widenSingletons(using Context): Type = dealias match { + def widenSingletons(skipSoftUnions: Boolean = false)(using Context): Type = dealias match { case tp: SingletonType => tp.widen case tp: OrType => - val tp1w = tp.widenSingletons + val tp1w = tp.widenSingletons(skipSoftUnions) if (tp1w eq tp) this else tp1w case tp: AndType => - val tp1w = tp.tp1.widenSingletons - val tp2w = tp.tp2.widenSingletons + val tp1w = tp.tp1.widenSingletons(skipSoftUnions) + val tp2w = tp.tp2.widenSingletons(skipSoftUnions) if ((tp.tp1 eq tp1w) && (tp.tp2 eq tp2w)) this else tp1w & tp2w case _ => this @@ -1798,7 +1833,7 @@ object Types extends TypeUtils { * no symbol it tries `member` as an alternative. */ def typeParamNamed(name: TypeName)(using Context): Symbol = - classSymbol.unforcedDecls.lookup(name) orElse member(name).symbol + classSymbol.unforcedDecls.lookup(name).orElse(member(name).symbol) /** If this is a prototype with some ignored component, reveal one more * layer of it. Otherwise the type itself. @@ -1931,9 +1966,9 @@ object Types extends TypeUtils { def annotatedToRepeated(using Context): Type = this match { case tp @ ExprType(tp1) => tp.derivedExprType(tp1.annotatedToRepeated) - case self @ AnnotatedType(tp, annot) if annot matches defn.RetainsByNameAnnot => + case self @ AnnotatedType(tp, annot) if annot.matches(defn.RetainsByNameAnnot) => self.derivedAnnotatedType(tp.annotatedToRepeated, annot) - case AnnotatedType(tp, annot) if annot matches defn.RepeatedAnnot => + case AnnotatedType(tp, annot) if annot.matches(defn.RepeatedAnnot) => val typeSym = tp.typeSymbol.asClass assert(typeSym == defn.SeqClass || typeSym == defn.ArrayClass) tp.translateParameterized(typeSym, defn.RepeatedParamClass) @@ -2245,7 +2280,7 @@ object Types extends TypeUtils { def _1: Type def _2: Designator - assert(NamedType.validPrefix(prefix), s"invalid prefix $prefix") + if !NamedType.validPrefix(prefix) then throw InvalidPrefix() private var myName: Name | Null = null private var lastDenotation: Denotation | Null = null @@ -2412,7 +2447,7 @@ object Types extends TypeUtils { else lastd match { case lastd: SymDenotation => if stillValid(lastd) && checkedPeriod.code != NowhereCode then finish(lastd.current) - else finish(memberDenot(lastd.initial.name, allowPrivate = false)) + else finish(memberDenot(lastd.initial.name, allowPrivate = lastd.is(Private))) case _ => fromDesignator } @@ -2633,9 +2668,9 @@ object Types extends TypeUtils { */ final def controlled[T](op: => T)(using Context): T = try { ctx.base.underlyingRecursions += 1 - if (ctx.base.underlyingRecursions < Config.LogPendingUnderlyingThreshold) + if ctx.base.underlyingRecursions < Config.LogPendingUnderlyingThreshold then op - else if (ctx.pendingUnderlying contains this) + else if ctx.pendingUnderlying.contains(this) then throw CyclicReference(symbol) else try { @@ -3022,6 +3057,8 @@ object Types extends TypeUtils { apply(prefix, designatorFor(prefix, name, denot)).withDenot(denot) } + class InvalidPrefix extends Exception + // --- Other SingletonTypes: ThisType/SuperType/ConstantType --------------------------- /** The type cls.this @@ -3375,8 +3412,8 @@ object Types extends TypeUtils { val bcs1set = BaseClassSet(bcs1) def recur(bcs2: List[ClassSymbol]): List[ClassSymbol] = bcs2 match { case bc2 :: bcs2rest => - if (bcs1set contains bc2) - if (bc2.is(Trait)) recur(bcs2rest) + if bcs1set.contains(bc2) then + if bc2.is(Trait) then recur(bcs2rest) else bcs1 // common class, therefore rest is the same in both sequences else bc2 :: recur(bcs2rest) case nil => bcs1 @@ -3472,9 +3509,8 @@ object Types extends TypeUtils { val bcs1set = BaseClassSet(bcs1) def recur(bcs2: List[ClassSymbol]): List[ClassSymbol] = bcs2 match { case bc2 :: bcs2rest => - if (bcs1set contains bc2) - if (bc2.is(Trait)) bc2 :: recur(bcs2rest) - else bcs2 + if bcs1set.contains(bc2) then + if bc2.is(Trait) then bc2 :: recur(bcs2rest) else bcs2 else recur(bcs2rest) case nil => bcs2 @@ -3532,8 +3568,8 @@ object Types extends TypeUtils { else tp1.atoms | tp2.atoms private def computeWidenSingletons()(using Context): Type = - val tp1w = tp1.widenSingletons - val tp2w = tp2.widenSingletons + val tp1w = tp1.widenSingletons() + val tp2w = tp2.widenSingletons() if ((tp1 eq tp1w) && (tp2 eq tp2w)) this else TypeComparer.lub(tp1w, tp2w, isSoft = isSoft) override def atoms(using Context): Atoms = @@ -3542,11 +3578,13 @@ object Types extends TypeUtils { if !isProvisional then atomsRunId = ctx.runId myAtoms - override def widenSingletons(using Context): Type = - if widenedRunId != ctx.runId then - myWidened = computeWidenSingletons() - if !isProvisional then widenedRunId = ctx.runId - myWidened + override def widenSingletons(skipSoftUnions: Boolean)(using Context): Type = + if !isSoft || skipSoftUnions then this + else + if widenedRunId != ctx.runId then + myWidened = computeWidenSingletons() + if !isProvisional then widenedRunId = ctx.runId + myWidened def derivedOrType(tp1: Type, tp2: Type, soft: Boolean = isSoft)(using Context): Type = if ((tp1 eq this.tp1) && (tp2 eq this.tp2) && soft == isSoft) this @@ -3730,9 +3768,51 @@ object Types extends TypeUtils { def integrate(tparams: List[ParamInfo], tp: Type)(using Context): Type = (tparams: @unchecked) match { case LambdaParam(lam, _) :: _ => tp.subst(lam, this) // This is where the precondition is necessary. - case params: List[Symbol @unchecked] => tp.subst(params, paramRefs) + case params: List[Symbol @unchecked] => IntegrateMap(params, paramRefs)(tp) } + /** A map that replaces references to symbols in `params` by the types in + * `paramRefs`. + * + * It is similar to [[Substituters#subst]] but avoids reloading denotations + * of named types by overriding `derivedSelect`. + * + * This is needed because during integration, [[TermParamRef]]s refer to a + * [[LambdaType]] that is not yet fully constructed, in particular for wich + * `paramInfos` is `null`. In that case all [[TermParamRef]]s have + * [[NoType]] as underlying type. Reloading denotions of selections + * involving such [[TermParamRef]]s in [[NamedType#withPrefix]] could then + * result in a [[NoDenotation]], which would make later disambiguation of + * overloads impossible. See `tests/pos/annot-17242.scala` for example. + */ + private class IntegrateMap(from: List[Symbol], to: List[Type])(using Context) extends TypeMap: + override def apply(tp: Type) = + // Same implementation as in `SubstMap`, except the `derivedSelect` in + // the `NamedType` case, and the default case that just calls `mapOver`. + tp match + case tp: NamedType => + val sym = tp.symbol + var fs = from + var ts = to + while (fs.nonEmpty && ts.nonEmpty) { + if (fs.head eq sym) return ts.head + fs = fs.tail + ts = ts.tail + } + if (tp.prefix `eq` NoPrefix) tp + else derivedSelect(tp, apply(tp.prefix)) + case _: BoundType | _: ThisType => tp + case _ => mapOver(tp) + + override final def derivedSelect(tp: NamedType, pre: Type): Type = + if tp.prefix eq pre then tp + else + pre match + case ref: ParamRef if (ref.binder eq self) && tp.symbol.exists && tp.symbol.is(Method) => + NamedType(pre, tp.name, tp.denot.asSeenFrom(pre)) + case _ => + tp.derivedSelect(pre) + final def derivedLambdaType(paramNames: List[ThisName] = this.paramNames, paramInfos: List[PInfo] = this.paramInfos, resType: Type = this.resType)(using Context): This = @@ -5413,11 +5493,11 @@ object Types extends TypeUtils { parent.hashIsStable override def eql(that: Type): Boolean = that match - case that: AnnotatedType => (parent eq that.parent) && (annot eql that.annot) + case that: AnnotatedType => (parent eq that.parent) && annot.eql(that.annot) case _ => false override def iso(that: Any, bs: BinderPairs): Boolean = that match - case that: AnnotatedType => parent.equals(that.parent, bs) && (annot eql that.annot) + case that: AnnotatedType => parent.equals(that.parent, bs) && annot.eql(that.annot) case _ => false } @@ -5792,11 +5872,13 @@ object Types extends TypeUtils { protected def derivedLambdaType(tp: LambdaType)(formals: List[tp.PInfo], restpe: Type): Type = tp.derivedLambdaType(tp.paramNames, formals, restpe) + protected def mapArg(arg: Type, tparam: ParamInfo): Type = arg match + case arg: TypeBounds => this(arg) + case arg => atVariance(variance * tparam.paramVarianceSign)(this(arg)) + protected def mapArgs(args: List[Type], tparams: List[ParamInfo]): List[Type] = args match case arg :: otherArgs if tparams.nonEmpty => - val arg1 = arg match - case arg: TypeBounds => this(arg) - case arg => atVariance(variance * tparams.head.paramVarianceSign)(this(arg)) + val arg1 = mapArg(arg, tparams.head) val otherArgs1 = mapArgs(otherArgs, tparams.tail) if ((arg1 eq arg) && (otherArgs1 eq otherArgs)) args else arg1 :: otherArgs1 @@ -5937,14 +6019,7 @@ object Types extends TypeUtils { } } - private def treeTypeMap = new TreeTypeMap( - typeMap = this, - // Using `ConservativeTreeCopier` is needed when copying dependent annoted - // types, where we can refer to a previous parameter represented as - // `TermParamRef` that has no underlying type yet. - // See tests/pos/annot-17242.scala. - cpy = ConservativeTreeCopier() - ) + private def treeTypeMap = new TreeTypeMap(typeMap = this) def mapOver(syms: List[Symbol]): List[Symbol] = mapSymbols(syms, treeTypeMap) @@ -5965,6 +6040,8 @@ object Types extends TypeUtils { override def stopAt = thisMap.stopAt def apply(tp: Type) = f(thisMap(tp)) } + + override def toString = s"${getClass.getSimpleName}@$hashCode" // otherwise would print as } /** A type map that maps also parents and self type of a ClassInfo */ @@ -5980,7 +6057,7 @@ object Types extends TypeUtils { } } - @sharable object IdentityTypeMap extends TypeMap()(NoContext) { + @sharable object IdentityTypeMap extends TypeMap()(using NoContext) { def apply(tp: Type): Type = tp } @@ -5994,9 +6071,30 @@ object Types extends TypeUtils { abstract class ApproximatingTypeMap(using Context) extends TypeMap { thisMap => protected def range(lo: Type, hi: Type): Type = - if (variance > 0) hi - else if (variance < 0) lo - else if (lo `eq` hi) lo + if variance > 0 then hi + else if variance < 0 then + if (lo eq defn.NothingType) then + // Approximate by Nothing & hi instead of just Nothing, in case the + // approximated type is used as the prefix of another type (this would + // lead to a type with a `NoDenotation` denot and a possible + // MissingType in `TypeErasure#sigName`). + // + // Note that we cannot simply check for a `Nothing` prefix in + // `derivedSelect`, because the substitution might be done lazily (for + // example if Nothing is the type of a parameter being depended on in + // a MethodType) + // + // Test case in tests/pos/i23530.scala (and tests/pos/i23627.scala for + // the higher-kinded case which requires eta-expansion) + hi.etaExpand match + case expandedHi: HKTypeLambda => + expandedHi.derivedLambdaType(resType = AndType(lo, expandedHi.resType)) + case _ => + // simple-kinded case + AndType(lo, hi) + else + lo + else if lo `eq` hi then lo else Range(lower(lo), upper(hi)) protected def emptyRange = range(defn.NothingType, defn.AnyType) @@ -6451,7 +6549,7 @@ object Types extends TypeUtils { def maybeAdd(xs: List[NamedType], tp: NamedType): List[NamedType] = if p(tp) then tp :: xs else xs val seen = util.HashSet[Type]() def apply(xs: List[NamedType], tp: Type): List[NamedType] = - if seen contains tp then xs + if seen.contains(tp) then xs else seen += tp tp match @@ -6630,7 +6728,7 @@ object Types extends TypeUtils { object fieldFilter extends NameFilter { def apply(pre: Type, name: Name)(using Context): Boolean = - name.isTermName && (pre member name).hasAltWith(!_.symbol.is(Method)) + name.isTermName && pre.member(name).hasAltWith(!_.symbol.is(Method)) def isStable = true } diff --git a/compiler/src/dotty/tools/dotc/core/Uniques.scala b/compiler/src/dotty/tools/dotc/core/Uniques.scala index da6b0aba88bd..b50905c22c98 100644 --- a/compiler/src/dotty/tools/dotc/core/Uniques.scala +++ b/compiler/src/dotty/tools/dotc/core/Uniques.scala @@ -3,6 +3,7 @@ package core import Types.*, Contexts.*, util.Stats.*, Hashable.*, Names.* import config.Config +import Symbols.Symbol import Decorators.* import util.{WeakHashSet, Stats} import WeakHashSet.Entry @@ -41,8 +42,10 @@ object Uniques: val h = doHash(null, designator, prefix) if monitored then recordCaching(h, classOf[NamedType]) def newType = - if (isTerm) new CachedTermRef(prefix, designator, h) - else new CachedTypeRef(prefix, designator, h) + try + if isTerm then new CachedTermRef(prefix, designator, h) + else new CachedTypeRef(prefix, designator, h) + catch case ex: InvalidPrefix => badPrefix(prefix, designator) if h == NotCached then newType else // Inlined from WeakHashSet#put @@ -61,6 +64,14 @@ object Uniques: linkedListLoop(oldHead) end if + end enterIfNew + + private def badPrefix(prefix: Type, desig: Designator)(using Context): Nothing = + def name = desig match + case desig: Name => desig + case desig: Symbol => desig.name + throw TypeError(em"invalid prefix $prefix when trying to form $prefix . $name") + end NamedTypeUniques final class AppliedUniques extends WeakHashSet[AppliedType](Config.initialUniquesCapacity * 2) with Hashable: diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileConstants.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileConstants.scala index 6ad71c5fd1ce..7a2232891a83 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileConstants.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileConstants.scala @@ -73,7 +73,10 @@ object ClassfileConstants { inline val CONSTANT_METHODHANDLE = 15 inline val CONSTANT_METHODTYPE = 16 + inline val CONSTANT_DYNAMIC = 17 inline val CONSTANT_INVOKEDYNAMIC = 18 + inline val CONSTANT_MODULE = 19 + inline val CONSTANT_PACKAGE = 20 // tags describing the type of a literal in attribute values inline val BYTE_TAG = 'B' diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index c20b6c73b1ea..d62b88bb31b5 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -194,27 +194,21 @@ class ClassfileParser( * Updates the read pointer of 'in'. */ def parseParents: List[Type] = { val superType = - if (classRoot.symbol == defn.ComparableClass || - classRoot.symbol == defn.JavaCloneableClass || - classRoot.symbol == defn.JavaSerializableClass) { - // Treat these interfaces as universal traits - in.nextChar + val superClass = in.nextChar + // Treat these interfaces as universal traits + if classRoot.symbol == defn.ComparableClass + || classRoot.symbol == defn.JavaCloneableClass + || classRoot.symbol == defn.JavaSerializableClass + then defn.AnyType - } else - pool.getSuperClass(in.nextChar).typeRef + pool.getSuperClass(superClass).typeRef val ifaceCount = in.nextChar - var ifaces = for (i <- (0 until ifaceCount).toList) yield pool.getSuperClass(in.nextChar).typeRef - // Dotty deviation: was - // var ifaces = for (i <- List.range(0, ifaceCount)) ... - // This does not typecheck because the type parameter of List is now lower-bounded by Int | Char. - // Consequently, no best implicit for the "Integral" evidence parameter of "range" - // is found. Previously, this worked because of weak conformance, which has been dropped. - + val ifaces = List.fill(ifaceCount.toInt): + pool.getSuperClass(in.nextChar).typeRef superType :: ifaces } - val result = unpickleOrParseInnerClasses() if (!result.isDefined) { var classInfo: Type = TempClassInfoType(parseParents, instanceScope, classRoot.symbol) @@ -233,8 +227,8 @@ class ClassfileParser( moduleRoot.setPrivateWithin(privateWithin) moduleRoot.sourceModule.setPrivateWithin(privateWithin) - for (i <- 0 until in.nextChar) parseMember(method = false) - for (i <- 0 until in.nextChar) parseMember(method = true) + for (_ <- 0 until in.nextChar) parseMember(method = false) + for (_ <- 0 until in.nextChar) parseMember(method = true) classRoot.registerCompanion(moduleRoot.symbol) moduleRoot.registerCompanion(classRoot.symbol) @@ -904,7 +898,7 @@ class ClassfileParser( // Nothing$ and Null$ were incorrectly emitted with a Scala attribute // instead of ScalaSignature before 2.13.0-M2, see https://github.com/scala/scala/pull/5952 - private val scalaUnpickleWhitelist = List(tpnme.nothingClass, tpnme.nullClass) + private val scalaUnpickleAllowlist = List(tpnme.nothingClass, tpnme.nullClass) /** Parse inner classes. Expects `in.bp` to point to the superclass entry. * Restores the old `bp`. @@ -1028,7 +1022,7 @@ class ClassfileParser( report.error(s"Found a TASTY attribute with a length different from 16 in $classfile. This is likely a bug in the compiler. Please report.", NoSourcePosition) } - if scan(tpnme.ScalaATTR) && !scalaUnpickleWhitelist.contains(classRoot.name) + if scan(tpnme.ScalaATTR) && !scalaUnpickleAllowlist.contains(classRoot.name) && !(classRoot.name.startsWith("Tuple") && classRoot.name.endsWith("$sp")) && !(classRoot.name.startsWith("Product") && classRoot.name.endsWith("$sp")) then @@ -1224,13 +1218,14 @@ class ClassfileParser( (in.nextByte.toInt: @switch) match { case CONSTANT_UTF8 | CONSTANT_UNICODE => in.skip(in.nextChar) - case CONSTANT_CLASS | CONSTANT_STRING | CONSTANT_METHODTYPE => + case CONSTANT_CLASS | CONSTANT_STRING | CONSTANT_METHODTYPE + | CONSTANT_MODULE | CONSTANT_PACKAGE => in.skip(2) case CONSTANT_METHODHANDLE => in.skip(3) case CONSTANT_FIELDREF | CONSTANT_METHODREF | CONSTANT_INTFMETHODREF | CONSTANT_NAMEANDTYPE | CONSTANT_INTEGER | CONSTANT_FLOAT - | CONSTANT_INVOKEDYNAMIC => + | CONSTANT_INVOKEDYNAMIC | CONSTANT_DYNAMIC => in.skip(4) case CONSTANT_LONG | CONSTANT_DOUBLE => in.skip(8) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 75fcaaf544a3..0ebe67834b36 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -630,8 +630,13 @@ class TreePickler(pickler: TastyPickler) { case tree: TypeTree => pickleType(tree.tpe) case SingletonTypeTree(ref) => - writeByte(SINGLETONtpt) - pickleTree(ref) + val tp = ref.tpe + val tp1 = tp.deskolemized + if tp1 ne tp then + pickleType(tp1) + else + writeByte(SINGLETONtpt) + pickleTree(ref) case RefinedTypeTree(parent, refinements) => if (refinements.isEmpty) pickleTree(parent) else { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index ee202330f172..4edbb278504e 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -127,7 +127,8 @@ class TreeUnpickler(reader: TastyReader, } } - class Completer(reader: TastyReader)(using @constructorOnly _ctx: Context) extends LazyType { + class Completer(reader: TastyReader)(using @constructorOnly _ctx: Context) + extends LazyType, CompleterWithCleanup { import reader.* val owner = ctx.owner val mode = ctx.mode @@ -147,6 +148,8 @@ class TreeUnpickler(reader: TastyReader, case ex: CyclicReference => throw ex case ex: AssertionError => fail(ex) case ex: Exception => fail(ex) + finally + cleanup() } class TreeReader(val reader: TastyReader) { @@ -1106,7 +1109,6 @@ class TreeUnpickler(reader: TastyReader, inline def readImportOrExport(inline mkTree: (Tree, List[untpd.ImportSelector]) => Tree)()(using Context): Tree = { val start = currentAddr - assert(sourcePathAt(start).isEmpty) readByte() readEnd() val expr = readTree() diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index f1f5222b73a1..de43df8e119d 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -64,9 +64,15 @@ object Scala2Unpickler { denot.info = PolyType.fromParams(denot.owner.typeParams, denot.info) } - def ensureConstructor(cls: ClassSymbol, clsDenot: ClassDenotation, scope: Scope)(using Context): Unit = { - if (scope.lookup(nme.CONSTRUCTOR) == NoSymbol) { - val constr = newDefaultConstructor(cls) + def ensureConstructor(cls: ClassSymbol, clsDenot: ClassDenotation, scope: Scope)(using Context): Unit = + doEnsureConstructor(cls, clsDenot, scope, fromScala2 = true) + + private def doEnsureConstructor(cls: ClassSymbol, clsDenot: ClassDenotation, scope: Scope, fromScala2: Boolean) + (using Context): Unit = + if scope.lookup(nme.CONSTRUCTOR) == NoSymbol then + val constr = + if fromScala2 || cls.isAllOf(Trait | JavaDefined) then newDefaultConstructor(cls) + else newConstructor(cls, Private, paramNames = Nil, paramTypes = Nil) // Scala 2 traits have a constructor iff they have initialization code // In dotc we represent that as !StableRealizable, which is also owner.is(NoInits) if clsDenot.flagsUNSAFE.is(Trait) then @@ -74,8 +80,6 @@ object Scala2Unpickler { clsDenot.setFlag(NoInits) addConstructorTypeParams(constr) cls.enter(constr, scope) - } - } def setClassInfo(denot: ClassDenotation, info: Type, fromScala2: Boolean, selfInfo: Type = NoType)(using Context): Unit = { val cls = denot.classSymbol @@ -109,7 +113,7 @@ object Scala2Unpickler { if (tsym.exists) tsym.setFlag(TypeParam) else denot.enter(tparam, decls) } - if (!denot.flagsUNSAFE.isAllOf(JavaModule)) ensureConstructor(cls, denot, decls) + if (!denot.flagsUNSAFE.isAllOf(JavaModule)) doEnsureConstructor(cls, denot, decls, fromScala2) val scalacCompanion = denot.classSymbol.scalacLinkedClass diff --git a/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala b/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala index bc04cc648a65..e6baafe9bdb7 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/TastyFileUtil.scala @@ -15,7 +15,7 @@ object TastyFileUtil { * package foo * class Foo * ``` - * then `getClassName("./out/foo/Foo.tasty") returns `Some("./out")` + * then `getClassPath("./out/foo/Foo.tasty") returns `Some("./out")` */ def getClassPath(file: AbstractFile): Option[String] = getClassName(file).map { className => @@ -37,13 +37,11 @@ object TastyFileUtil { assert(file.extension == "tasty") val bytes = file.toByteArray val names = new TastyClassName(bytes).readName() - names.map { case (packageName, className) => - val fullName = packageName match { - case EMPTY_PACKAGE => s"${className.lastPart}" - case _ => s"$packageName.${className.lastPart}" - } - fullName - } + names.map: (packageName, className) => + if packageName == EMPTY_PACKAGE then + s"${className.lastPart.encode}" + else + s"${packageName.encode}.${className.lastPart.encode}" } } diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index d83817dd90e0..d5b10f853784 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -96,9 +96,19 @@ object Inliner: } end isElideableExpr + // InlineCopier is a more fault-tolerant copier that does not cause errors when + // function types in applications are undefined. This is necessary since we copy at + // the same time as establishing the proper context in which the copied tree should + // be evaluated. This matters for opaque types, see neg/i14653.scala. + private class InlineCopier() extends TypedTreeCopier: + override def Apply(tree: Tree)(fun: Tree, args: List[Tree])(using Context): Apply = + if fun.tpe.widen.exists then super.Apply(tree)(fun, args) + else untpd.cpy.Apply(tree)(fun, args).withTypeUnchecked(tree.tpe) + // InlinerMap is a TreeTypeMap with special treatment for inlined arguments: // They are generally left alone (not mapped further, and if they wrap a type - // the type Inlined wrapper gets dropped + // the type Inlined wrapper gets dropped. + // As a side effect, register @nowarn annotations from annotated expressions. private class InlinerMap( typeMap: Type => Type, treeMap: Tree => Tree, @@ -107,13 +117,24 @@ object Inliner: substFrom: List[Symbol], substTo: List[Symbol])(using Context) extends TreeTypeMap( - typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, - // It is necessary to use the `ConservativeTreeCopier` since we copy at - // the same time as establishing the proper context in which the copied - // tree should be evaluated. This matters for opaque types, see - // neg/i14653.scala. - ConservativeTreeCopier() - ): + typeMap, treeMap, oldOwners, newOwners, substFrom, substTo, InlineCopier()): + + override def transform(tree: Tree)(using Context): Tree = + tree match + case Typed(expr, tpt) => + def loop(tpe: Type): Unit = + tpe match + case AnnotatedType(parent, annot) => + if annot.hasSymbol(defn.NowarnAnnot) then + val argPos = annot.argument(0).getOrElse(tree).sourcePos + val conf = annot.argumentConstantString(0).getOrElse("") + ctx.run.nn.suppressions.registerNowarn(tree.sourcePos, expr.span)(conf, argPos) + else + loop(parent) + case _ => + loop(tpt.tpe) + case _ => + super.transform(tree) override def copy( typeMap: Type => Type, @@ -377,7 +398,17 @@ class Inliner(val call: tpd.Tree)(using Context): * type aliases, add proxy definitions to `opaqueProxies` that expose these aliases. */ private def addOpaqueProxies(tp: Type, span: Span, forThisProxy: Boolean)(using Context): Unit = - tp.foreachPart { + val foreachTpPart = + (p: Type => Unit) => + if forThisProxy then + // Performs operations on all parts of this type, outside of the applied type arguments + new ForeachAccumulator(p, StopAt.None) { + override def apply(x: Unit, tp: Type) = tp match + case AppliedType(tycon, _) => super.apply(x, tycon) + case other => super.apply(x, other) + }.apply((), tp) + else tp.foreachPart(p) + foreachTpPart { case ref: TermRef => for cls <- ref.widen.baseClasses do if cls.containsOpaques @@ -1067,9 +1098,9 @@ class Inliner(val call: tpd.Tree)(using Context): if suspendable then ctx.compilationUnit.suspend() // this throws a SuspendException - val evaluatedSplice = inContext(quoted.MacroExpansion.context(inlinedFrom)) { - Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext) - } + val evaluatedSplice = + inContext(quoted.MacroExpansion.context(inlinedFrom)): + Splicer.splice(body, splicePos, inlinedFrom.srcPos, MacroClassLoader.fromContext) val inlinedNormalizer = new TreeMap { override def transform(tree: tpd.Tree)(using Context): tpd.Tree = tree match { case tree @ Inlined(_, Nil, expr) if tree.inlinedFromOuterScope && enclosingInlineds.isEmpty => transform(expr) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index e9ac2b92a488..f535a7925451 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -23,6 +23,7 @@ import collection.mutable import reporting.{NotConstant, trace} import util.Spans.Span import dotty.tools.dotc.core.Periods.PhaseId +import dotty.tools.dotc.util.chaining.* /** Support for querying inlineable methods and for inlining calls to such methods */ object Inlines: @@ -44,6 +45,11 @@ object Inlines: def bodyToInline(sym: SymDenotation)(using Context): Tree = if hasBodyToInline(sym) then sym.getAnnotation(defn.BodyAnnot).get.tree + .tap: body => + for annot <- sym.getAnnotation(defn.NowarnAnnot) do + val argPos = annot.argument(0).getOrElse(annot.tree).sourcePos + val conf = annot.argumentConstantString(0).getOrElse("") + ctx.run.nn.suppressions.registerNowarn(annot.tree.sourcePos, body.span)(conf, argPos) else EmptyTree @@ -95,7 +101,7 @@ object Inlines: * @return An `Inlined` node that refers to the original call and the inlined bindings * and body that replace it. */ - def inlineCall(tree: Tree)(using Context): Tree = + def inlineCall(tree: Tree)(using Context): Tree = ctx.profiler.onInlineCall(tree.symbol): if tree.symbol.denot != SymDenotations.NoDenotation && tree.symbol.effectiveOwner == defn.CompiletimeTestingPackage.moduleClass then @@ -387,6 +393,11 @@ object Inlines: case ConstantType(Constant(code: String)) => val unitName = "tasty-reflect" val source2 = SourceFile.virtual(unitName, code) + def compilationUnits(untpdTree: untpd.Tree, tpdTree: Tree): List[CompilationUnit] = + val compilationUnit = CompilationUnit(unitName, code) + compilationUnit.tpdTree = tpdTree + compilationUnit.untpdTree = untpdTree + List(compilationUnit) // We need a dummy owner, as the actual one does not have a computed denotation yet, // but might be inspected in a transform phase, leading to cyclic errors val dummyOwner = newSymbol(ctx.owner, "$dummySymbol$".toTermName, Private, defn.AnyType, NoSymbol) @@ -394,31 +405,30 @@ object Inlines: ctx.fresh.setNewTyperState().setTyper(new Typer(ctx.nestingLevel + 1)).setSource(source2) inContext(newContext) { - val tree2 = new Parser(source2).block() - if ctx.reporter.allErrors.nonEmpty then + def noErrors = ctx.reporter.allErrors.isEmpty + val untpdTree = new Parser(source2).block() + if !noErrors then ctx.reporter.allErrors.map((ErrorKind.Parser, _)) else - val tree3 = ctx.typer.typed(tree2) + val tpdTree1 = ctx.typer.typed(untpdTree) ctx.base.postTyperPhase match - case postTyper: PostTyper if ctx.reporter.allErrors.isEmpty => - val tree4 = atPhase(postTyper) { postTyper.newTransformer.transform(tree3) } + case postTyper: PostTyper if noErrors => + val tpdTree2 = + atPhase(postTyper) { postTyper.runOn(compilationUnits(untpdTree, tpdTree1)).head.tpdTree } ctx.base.setRootTreePhase match - case setRootTree => - val tree5 = - val compilationUnit = CompilationUnit(unitName, code) - compilationUnit.tpdTree = tree4 - compilationUnit.untpdTree = tree2 - var units = List(compilationUnit) - atPhase(setRootTree)(setRootTree.runOn(units).head.tpdTree) + case setRootTree if noErrors => // might be noPhase, if -Yretain-trees is not used + val tpdTree3 = + atPhase(setRootTree)(setRootTree.runOn(compilationUnits(untpdTree, tpdTree2)).head.tpdTree) ctx.base.inliningPhase match - case inlining: Inlining if ctx.reporter.allErrors.isEmpty => - val tree6 = atPhase(inlining) { inlining.newTransformer.transform(tree5) } - if ctx.reporter.allErrors.isEmpty && reconstructedTransformPhases.nonEmpty then - var transformTree = tree6 + case inlining: Inlining if noErrors => + val tpdTree4 = atPhase(inlining) { inlining.newTransformer.transform(tpdTree3) } + if noErrors && reconstructedTransformPhases.nonEmpty then + var transformTree = tpdTree4 for phase <- reconstructedTransformPhases do - if ctx.reporter.allErrors.isEmpty then + if noErrors then transformTree = atPhase(phase.end + 1)(phase.transformUnit(transformTree)) case _ => + case _ => case _ => ctx.reporter.allErrors.map((ErrorKind.Typer, _)) } diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index af27a4006140..c54a81372afa 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -46,6 +46,16 @@ case class Completion(label: String, description: String, symbols: List[Symbol]) object Completion: + def scopeContext(pos: SourcePosition)(using Context): CompletionResult = + val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) + val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase) + inContext(completionContext): + val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos) + val rawPrefix = completionPrefix(untpdPath, pos) + // Lazy mode is to avoid too many checks as it's mostly for printing types + val completer = new Completer(Mode.Lazy, pos, untpdPath, _ => true) + completer.scopeCompletions + /** Get possible completions from tree at `pos` * * @return offset and list of symbols for possible completions @@ -58,7 +68,6 @@ object Completion: val mode = completionMode(untpdPath, pos) val rawPrefix = completionPrefix(untpdPath, pos) val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath) - postProcessCompletions(untpdPath, completions, rawPrefix) /** Get possible completions from tree at `pos` @@ -88,19 +97,21 @@ object Completion: * Otherwise, provide no completion suggestion. */ def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = path match + // Ignore `package foo@@` and `package foo.bar@@` + case ((_: tpd.Select) | (_: tpd.Ident)):: (_ : tpd.PackageDef) :: _ => Mode.None case GenericImportSelector(sel) => if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@ else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport else Mode.None // import scala.{util => u@@} case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@ + case untpd.InterpolatedString(_, untpd.Literal(Constants.Constant(_: String)) :: _) :: _ => + Mode.Term | Mode.Scope case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions case (ref: untpd.RefTree) :: _ => val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope - if (ref.name.isTermName) Mode.Term | maybeSelectMembers else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers else Mode.None - case _ => Mode.None /** When dealing with in varios palces we check to see if they are @@ -145,11 +156,12 @@ object Completion: checkBacktickPrefix(ident.source.content(), ident.span.start, ident.span.end) case (tree: untpd.RefTree) :: _ if tree.name != nme.ERROR => - tree.name.toString.take(pos.span.point - tree.span.point) - - case _ => naiveCompletionPrefix(pos.source.content().mkString, pos.point) - + val nameStart = tree.span.point + val start = if pos.source.content().lift(nameStart).contains('`') then nameStart + 1 else nameStart + tree.name.toString.take(pos.span.point - start) + case _ => + naiveCompletionPrefix(pos.source.content().mkString, pos.point) end completionPrefix private object GenericImportSelector: @@ -166,6 +178,14 @@ object Completion: case (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport) case _ => None + private object StringContextApplication: + def unapply(path: List[tpd.Tree]): Option[tpd.Apply] = + path match + case tpd.Select(qual @ tpd.Apply(tpd.Select(tpd.Select(_, nme.StringContext), _), _), _) :: _ => + Some(qual) + case _ => None + + /** Inspect `path` to determine the offset where the completion result should be inserted. */ def completionOffset(untpdPath: List[untpd.Tree]): Int = untpdPath match @@ -214,11 +234,12 @@ object Completion: val result = adjustedPath match // Ignore synthetic select from `This` because in code it was `Ident` // See example in dotty.tools.languageserver.CompletionTest.syntheticThis - case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions - case tpd.Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual) - case tpd.Select(qual, _) :: _ => Map.empty + case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions.names + case StringContextApplication(qual) => + completer.scopeCompletions.names ++ completer.selectionCompletions(qual) + case tpd.Select(qual, _) :: _ => completer.selectionCompletions(qual) case (tree: tpd.ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) - case _ => completer.scopeCompletions + case _ => completer.scopeCompletions.names interactiv.println(i"""completion info with pos = $pos, | term = ${completer.mode.is(Mode.Term)}, @@ -300,15 +321,15 @@ object Completion: * 8. symbol is not a constructor proxy module when in type completion mode * 9. have same term/type kind as name prefix given so far */ - def isValidCompletionSymbol(sym: Symbol, completionMode: Mode, isNew: Boolean)(using Context): Boolean = - + def isValidCompletionSymbol(sym: Symbol, completionMode: Mode, isNew: Boolean)(using Context): Boolean = try lazy val isEnum = sym.is(Enum) || (sym.companionClass.exists && sym.companionClass.is(Enum)) sym.exists && - !sym.isAbsent() && + !sym.isAbsent(canForce = false) && !sym.isPrimaryConstructor && - sym.sourceSymbol.exists && + // running sourceSymbol on ExportedTerm will force a lot of computation from collectSubTrees + (sym.is(ExportedTerm) || sym.sourceSymbol.exists) && (!sym.is(Package) || sym.is(ModuleClass)) && !sym.isAllOf(Mutable | Accessor) && !sym.isPackageObject && @@ -319,6 +340,10 @@ object Completion: (completionMode.is(Mode.Term) && (sym.isTerm || sym.is(ModuleClass)) || (completionMode.is(Mode.Type) && (sym.isType || sym.isStableMember))) ) + catch + case NonFatal(ex) => + false + end isValidCompletionSymbol given ScopeOrdering(using Context): Ordering[Seq[SingleDenotation]] with val order = @@ -338,7 +363,7 @@ object Completion: * For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map * and they never conflict with each other. */ - class Completer(val mode: Mode, pos: SourcePosition, untpdPath: List[untpd.Tree], matches: Name => Boolean): + class Completer(val mode: Mode, pos: SourcePosition, untpdPath: List[untpd.Tree], matches: Name => Boolean)(using Context): /** Completions for terms and types that are currently in scope: * the members of the current class, local definitions and the symbols that have been imported, * recursively adding completions from outer scopes. @@ -352,7 +377,7 @@ object Completion: * (even if the import follows it syntactically) * - a more deeply nested import shadowing a member or a local definition causes an ambiguity */ - def scopeCompletions(using context: Context): CompletionMap = + lazy val scopeCompletions: CompletionResult = /** Temporary data structure representing denotations with the same name introduced in a given scope * as a member of a type, by a local definition or by an import clause @@ -363,14 +388,19 @@ object Completion: ScopedDenotations(denots.filter(includeFn), ctx) val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty) + val renames = collection.mutable.Map.empty[Symbol, Name] def addMapping(name: Name, denots: ScopedDenotations) = mappings(name) = mappings(name) :+ denots ctx.outersIterator.foreach { case ctx @ given Context => if ctx.isImportContext then - importedCompletions.foreach { (name, denots) => + val imported = importedCompletions + imported.names.foreach { (name, denots) => addMapping(name, ScopedDenotations(denots, ctx, include(_, name))) } + imported.renames.foreach { (name, newName) => + renames(name) = newName + } else if ctx.owner.isClass then accessibleMembers(ctx.owner.thisType) .groupByName.foreach { (name, denots) => @@ -414,7 +444,6 @@ object Completion: // most deeply nested member or local definition if not shadowed by an import case Some(local) if local.ctx.scope == first.ctx.scope => resultMappings += name -> local.denots - case None if isSingleImport || isImportedInDifferentScope || isSameSymbolImportedDouble => resultMappings += name -> first.denots case None if notConflictingWithDefaults => @@ -424,7 +453,7 @@ object Completion: } } - resultMappings + CompletionResult(resultMappings, renames.toMap) end scopeCompletions /** Widen only those types which are applied or are exactly nothing @@ -442,9 +471,15 @@ object Completion: def selectionCompletions(qual: tpd.Tree)(using Context): CompletionMap = val adjustedQual = widenQualifier(qual) - implicitConversionMemberCompletions(adjustedQual) ++ + if qual.symbol.is(Package) then + directMemberCompletions(adjustedQual) + else if qual.typeOpt.hasSimpleKind then + implicitConversionMemberCompletions(adjustedQual) ++ extensionCompletions(adjustedQual) ++ directMemberCompletions(adjustedQual) + else + Map.empty + /** Completions for members of `qual`'s type. * These include inherited definitions but not members added by extensions or implicit conversions @@ -458,15 +493,20 @@ object Completion: /** Completions introduced by imports directly in this context. * Completions from outer contexts are not included. */ - private def importedCompletions(using Context): CompletionMap = + private def importedCompletions(using Context): CompletionResult = val imp = ctx.importInfo + val renames = collection.mutable.Map.empty[Symbol, Name] if imp == null then - Map.empty + CompletionResult(Map.empty, Map.empty) else def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = imp.site.member(name).alternatives - .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } + .collect { case denot if include(denot, nameInScope) => + if name != nameInScope then + renames(denot.symbol) = nameInScope + nameInScope -> denot + } val givenImports = imp.importedImplicits .map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) } @@ -492,7 +532,8 @@ object Completion: fromImport(original.toTypeName, nameInScope.toTypeName) }.toSeq.groupByName - givenImports ++ wildcardMembers ++ explicitMembers + val results = givenImports ++ wildcardMembers ++ explicitMembers + CompletionResult(results, renames.toMap) end importedCompletions /** Completions from implicit conversions including old style extensions using implicit classes */ @@ -545,10 +586,10 @@ object Completion: // There are four possible ways for an extension method to be applicable // 1. The extension method is visible under a simple name, by being defined or inherited or imported in a scope enclosing the reference. - val termCompleter = new Completer(Mode.Term, pos, untpdPath, matches) - val extMethodsInScope = termCompleter.scopeCompletions.toList.flatMap: - case (name, denots) => denots.collect: - case d: SymDenotation if d.isTerm && d.termRef.symbol.is(Extension) => (d.termRef, name.asTermName) + val extMethodsInScope = scopeCompletions.names.toList.flatMap: + case (name, denots) => + denots.collect: + case d if d.isTerm && d.symbol.is(Extension) => (d.symbol.termRef, name.asTermName) // 2. The extension method is a member of some given instance that is visible at the point of the reference. val givensInScope = ctx.implicits.eligible(defn.AnyType).map(_.implicitRef.underlyingRef) @@ -581,7 +622,7 @@ object Completion: private def include(denot: SingleDenotation, nameInScope: Name)(using Context): Boolean = matches(nameInScope) && completionsFilter(NoType, nameInScope) && - isValidCompletionSymbol(denot.symbol, mode, isNew) + (mode.is(Mode.Lazy) || isValidCompletionSymbol(denot.symbol, mode, isNew)) private def extractRefinements(site: Type)(using Context): Seq[SingleDenotation] = site match @@ -611,7 +652,7 @@ object Completion: val members = site.memberDenots(completionsFilter, appendMemberSyms).collect { case mbr if include(mbr, mbr.name) - && mbr.symbol.isAccessibleFrom(site) => mbr + && (mode.is(Mode.Lazy) || mbr.symbol.isAccessibleFrom(site)) => mbr } val refinements = extractRefinements(site).filter(mbr => include(mbr, mbr.name)) @@ -648,6 +689,7 @@ object Completion: private type CompletionMap = Map[Name, Seq[SingleDenotation]] + case class CompletionResult(names: Map[Name, Seq[SingleDenotation]], renames: Map[Symbol, Name]) /** * The completion mode: defines what kinds of symbols should be included in the completion * results. @@ -673,3 +715,5 @@ object Completion: val Member: Mode = new Mode(16) + val Lazy: Mode = new Mode(32) + diff --git a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala index a03ae502f2f1..f1db0c949cef 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala @@ -144,6 +144,7 @@ object Interactive { ( sym == tree.symbol || sym.exists && sym == tree.symbol.sourceSymbol + || sym.exists && sym.sourceSymbol == tree.symbol || !include.isEmpty && sym.name == tree.symbol.name && sym.maybeOwner != tree.symbol.maybeOwner && ( include.isOverridden && overrides(sym, tree.symbol) || include.isOverriding && overrides(tree.symbol, sym) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 092ab2eeea0d..1556b31e4ce8 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -254,7 +254,8 @@ object JavaParsers { t } - def optArrayBrackets(tpt: Tree): Tree = + def optArrayBrackets(tpt: Tree): Tree = { + annotations() if (in.token == LBRACKET) { val tpt1 = atSpan(tpt.span.start, in.offset) { arrayOf(tpt) } in.nextToken() @@ -262,6 +263,7 @@ object JavaParsers { optArrayBrackets(tpt1) } else tpt + } def basicType(): Tree = atSpan(in.offset) { @@ -335,7 +337,7 @@ object JavaParsers { } def annotations(): List[Tree] = { - var annots = new ListBuffer[Tree] + val annots = new ListBuffer[Tree] while (in.token == AT) { in.nextToken() annotation() match { diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 4bd7769187d5..03bba910e348 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -33,6 +33,7 @@ import config.Feature import config.Feature.{sourceVersion, migrateTo3, globalOnlyImports} import config.SourceVersion.* import config.SourceVersion +import dotty.tools.dotc.util.chaining.* object Parsers { @@ -66,7 +67,6 @@ object Parsers { this == Given || this == ExtensionFollow def acceptsVariance = this == Class || this == CaseClass || this == Type - end ParamOwner enum ParseKind: @@ -96,6 +96,9 @@ object Parsers { private val InCase: Region => Region = Scanners.InCase(_) private val InCond: Region => Region = Scanners.InParens(LPAREN, _) private val InFor : Region => Region = Scanners.InBraces(_) + private val InOldCond: Region => Region = // old-style Cond to allow indent when InParens, see #22608 + case p: Scanners.InParens => Scanners.Indented(p.indentWidth, p.prefix, p) + case r => r abstract class ParserCommon(val source: SourceFile)(using Context) { @@ -676,7 +679,7 @@ object Parsers { def checkNextNotIndented(): Unit = if in.isNewLine then val nextIndentWidth = in.indentWidth(in.next.offset) - if in.currentRegion.indentWidth < nextIndentWidth then + if in.currentRegion.indentWidth < nextIndentWidth && in.currentRegion.closedBy == OUTDENT then warning(em"Line is indented too far to the right, or a `{` or `:` is missing", in.next.offset) /* -------- REWRITES ----------------------------------------------------------- */ @@ -688,16 +691,17 @@ object Parsers { def testChar(idx: Int, p: Char => Boolean): Boolean = { val txt = source.content - idx < txt.length && p(txt(idx)) + idx >= 0 && idx < txt.length && p(txt(idx)) } def testChar(idx: Int, c: Char): Boolean = { val txt = source.content - idx < txt.length && txt(idx) == c + idx >= 0 && idx < txt.length && txt(idx) == c } def testChars(from: Int, str: String): Boolean = - str.isEmpty || + str.isEmpty + || testChar(from, str.head) && testChars(from + 1, str.tail) def skipBlanks(idx: Int, step: Int = 1): Int = @@ -1020,10 +1024,11 @@ object Parsers { */ def followingIsLambdaAfterColon(): Boolean = val lookahead = in.LookaheadScanner(allowIndent = true) + .tap(_.currentRegion.knownWidth = in.currentRegion.indentWidth) def isArrowIndent() = lookahead.isArrow && { - lookahead.nextToken() + lookahead.observeArrowIndented() lookahead.token == INDENT || lookahead.token == EOF } lookahead.nextToken() @@ -2127,25 +2132,25 @@ object Parsers { def condExpr(altToken: Token): Tree = val t: Tree = if in.token == LPAREN then - var t: Tree = atSpan(in.offset): - makeTupleOrParens(inParensWithCommas(commaSeparated(exprInParens))) - if in.token != altToken then - if toBeContinued(altToken) then - t = inSepRegion(InCond) { + inSepRegion(InOldCond): // allow inferred NEWLINE for observeIndented below + atSpan(in.offset): + makeTupleOrParens(inParensWithCommas(commaSeparated(exprInParens))) + .pipe: t => + if in.token == altToken then t + else if toBeContinued(altToken) then + inSepRegion(InCond): expr1Rest( postfixExprRest( simpleExprRest(t, Location.ElseWhere), Location.ElseWhere), Location.ElseWhere) - } else if rewriteToNewSyntax(t.span) then - dropParensOrBraces(t.span.start, s"${tokenString(altToken)}") + dropParensOrBraces(t.span.start, tokenString(altToken)) in.observeIndented() return t - t else if in.isNestedStart then - try expr() finally newLinesOpt() + expr().tap(_ => newLinesOpt()) else inSepRegion(InCond)(expr()) if rewriteToOldSyntax(t.span.startPos) then revertToParens(t) @@ -2954,14 +2959,18 @@ object Parsers { def pattern1(location: Location = Location.InPattern): Tree = val p = pattern2() if in.isColon then - val isVariableOrNumber = isVarPattern(p) || p.isInstanceOf[Number] + val isVariable = unsplice(p) match { + case x: Ident => x.name.isVarPattern + case _ => false + } + val isVariableOrNumber = isVariable || p.isInstanceOf[Number] if !isVariableOrNumber then report.gradualErrorOrMigrationWarning( em"""Type ascriptions after patterns other than: | * variable pattern, e.g. `case x: String =>` | * number literal pattern, e.g. `case 10.5: Double =>` |are no longer supported. Remove the type ascription or move it to a separate variable pattern.""", - in.sourcePos(), + p.sourcePos, warnFrom = `3.3`, errorFrom = future ) @@ -3273,7 +3282,7 @@ object Parsers { ok def typeParam(): TypeDef = { - val isAbstractOwner = paramOwner == ParamOwner.Type || paramOwner == ParamOwner.TypeParam + val isAbstractOwner = (paramOwner == ParamOwner.Type || paramOwner == ParamOwner.TypeParam) val start = in.offset var mods = annotsAsMods() | Param if paramOwner == ParamOwner.Class || paramOwner == ParamOwner.CaseClass then @@ -3294,7 +3303,13 @@ object Parsers { } else ident().toTypeName val hkparams = typeParamClauseOpt(ParamOwner.Type) - val bounds = if (isAbstractOwner) typeBounds() else typeParamBounds(name) + // val bounds = if (isAbstractOwner) typeBounds() else typeParamBounds(name) + val bounds = typeParamBounds(name) match + case bounds: TypeBoundsTree => bounds + case bounds: ContextBounds if !isAbstractOwner => bounds + case ContextBounds(bounds, cxBounds) => + for cbound <- cxBounds do report.error(IllegalContextBounds(), cbound.srcPos) + bounds TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods) } } @@ -3402,7 +3417,10 @@ object Parsers { // begin termParamClause inParensWithCommas { if in.token == RPAREN && paramOwner != ParamOwner.ExtensionPrefix && !impliedMods.is(Given) - then Nil + then + if paramOwner.takesOnlyUsingClauses then + syntaxError(em"`using` expected") + Nil else val clause = if paramOwner == ParamOwner.ExtensionPrefix @@ -4050,7 +4068,10 @@ object Parsers { leadParamss += extParams isUsingClause(extParams) do () - leadParamss ++= termParamClauses(ParamOwner.ExtensionFollow, numLeadParams) + // Empty parameter clauses are filtered out. They are already reported as syntax errors and are not + // allowed here. + val extFollowParams = termParamClauses(ParamOwner.ExtensionFollow, numLeadParams).filterNot(_.isEmpty) + leadParamss ++= extFollowParams if in.isColon then syntaxError(em"no `:` expected here") in.nextToken() diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index aadedde612f7..1992ab32c9c6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -601,6 +601,20 @@ object Scanners { lastWidth = r.knownWidth newlineIsSeparating = r.isInstanceOf[InBraces] + // can emit OUTDENT if line is not non-empty blank line at EOF + inline def isTrailingBlankLine: Boolean = + token == EOF && { + val end = buf.length - 1 // take terminal NL as empty last line + val prev = buf.lastIndexWhere(!isWhitespace(_), end = end) + prev < 0 || end - prev > 0 && isLineBreakChar(buf(prev)) + } + + inline def canDedent: Boolean = + lastToken != INDENT + && !isLeadingInfixOperator(nextWidth) + && !statCtdTokens.contains(lastToken) + && !isTrailingBlankLine + if newlineIsSeparating && canEndStatTokens.contains(lastToken) && canStartStatTokens.contains(token) @@ -613,7 +627,7 @@ object Scanners { || nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then if currentRegion.isOutermost then if nextWidth < lastWidth then currentRegion = topLevelRegion(nextWidth) - else if !isLeadingInfixOperator(nextWidth) && !statCtdTokens.contains(lastToken) && lastToken != INDENT then + else if canDedent then currentRegion match case r: Indented => insert(OUTDENT, offset) @@ -635,7 +649,6 @@ object Scanners { insert(OUTDENT, offset) else if r.isInstanceOf[InBraces] && !closingRegionTokens.contains(token) then report.warning("Line is indented too far to the left, or a `}` is missing", sourcePos()) - else if lastWidth < nextWidth || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then if canStartIndentTokens.contains(lastToken) then @@ -655,7 +668,7 @@ object Scanners { def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth): Message = em"""Incompatible combinations of tabs and spaces in indentation prefixes. |Previous indent : $lastWidth - |Latest indent : $nextWidth""" + |Latest indent : $nextWidth""" def observeColonEOL(inTemplate: Boolean): Unit = val enabled = @@ -669,6 +682,23 @@ object Scanners { reset() if atEOL then token = COLONeol + // consume => and insert if applicable. Used to detect colon arrow: x => + def observeArrowIndented(): Unit = + if isArrow && indentSyntax then + peekAhead() + val atEOL = isAfterLineEnd + val atEOF = token == EOF + reset() + if atEOF then + token = EOF + else if atEOL then + val nextWidth = indentWidth(next.offset) + val lastWidth = currentRegion.indentWidth + if lastWidth < nextWidth then + currentRegion = Indented(nextWidth, COLONeol, currentRegion) + offset = next.offset + token = INDENT + def observeIndented(): Unit = if indentSyntax && isNewLine then val nextWidth = indentWidth(next.offset) @@ -1097,7 +1127,7 @@ object Scanners { reset() next - class LookaheadScanner(val allowIndent: Boolean = false) extends Scanner(source, offset, allowIndent = allowIndent) { + class LookaheadScanner(allowIndent: Boolean = false) extends Scanner(source, offset, allowIndent = allowIndent) { override protected def initialCharBufferSize = 8 override def languageImportContext = Scanner.this.languageImportContext } @@ -1649,7 +1679,7 @@ object Scanners { case class InCase(outer: Region) extends Region(OUTDENT) /** A class describing an indentation region. - * @param width The principal indendation width + * @param width The principal indentation width * @param prefix The token before the initial of the region */ case class Indented(width: IndentWidth, prefix: Token, outer: Region | Null) extends Region(OUTDENT): diff --git a/compiler/src/dotty/tools/dotc/printing/Formatting.scala b/compiler/src/dotty/tools/dotc/printing/Formatting.scala index c88f71f2d07d..9c90a7746918 100644 --- a/compiler/src/dotty/tools/dotc/printing/Formatting.scala +++ b/compiler/src/dotty/tools/dotc/printing/Formatting.scala @@ -129,6 +129,7 @@ object Formatting { given Show[Class[?]] = ShowAny given Show[Throwable] = ShowAny given Show[StringBuffer] = ShowAny + given Show[StringBuilder] = ShowAny given Show[CompilationUnit] = ShowAny given Show[Phases.Phase] = ShowAny given Show[TyperState] = ShowAny diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 4fdfd7cb3e60..26936361cb2f 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -159,7 +159,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(GlobalPrec) { val argStr: Text = if args.length == 2 - && !defn.isTupleNType(args.head) + && !defn.isDirectTupleNType(args.head) && !isGiven then atPrec(InfixPrec) { argText(args.head) } @@ -1040,7 +1040,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { def recur(t: untpd.Tree): Text = t match case Apply(fn, Nil) => recur(fn) case Apply(fn, args) => - val explicitArgs = args.filterNot(untpd.stripNamedArg(_).symbol.name.is(DefaultGetterName)) + val explicitArgs = args.filterNot(_.symbol.name.is(DefaultGetterName)) recur(fn) ~ "(" ~ toTextGlobal(explicitArgs, ", ") ~ ")" case TypeApply(fn, args) => recur(fn) ~ "[" ~ toTextGlobal(args, ", ") ~ "]" case Select(qual, nme.CONSTRUCTOR) => recur(qual) diff --git a/compiler/src/dotty/tools/dotc/printing/Texts.scala b/compiler/src/dotty/tools/dotc/printing/Texts.scala index 475e2c6900d5..5909f8171843 100644 --- a/compiler/src/dotty/tools/dotc/printing/Texts.scala +++ b/compiler/src/dotty/tools/dotc/printing/Texts.scala @@ -139,7 +139,8 @@ object Texts { def mkString(width: Int, withLineNumbers: Boolean): String = { val sb = new StringBuilder - val numberWidth = if (withLineNumbers) (2 * maxLine.toString.length) + 2 else 0 + // width display can be upto a range "n-n" where 1 <= n <= maxLine+1 + val numberWidth = if (withLineNumbers) (2 * (maxLine + 1).toString.length) + 2 else 0 layout(width - numberWidth).print(sb, numberWidth) sb.toString } diff --git a/compiler/src/dotty/tools/dotc/profile/Profiler.scala b/compiler/src/dotty/tools/dotc/profile/Profiler.scala index ab3e73468385..8b1192871cff 100644 --- a/compiler/src/dotty/tools/dotc/profile/Profiler.scala +++ b/compiler/src/dotty/tools/dotc/profile/Profiler.scala @@ -117,12 +117,12 @@ sealed trait Profiler { protected def beforeImplicitSearch(pt: Type): TracedEventId = TracedEventId.Empty protected def afterImplicitSearch(event: TracedEventId): Unit = () - inline def onMacroSplice[T](macroSym: Symbol)(inline body: T): T = - val event = beforeMacroSplice(macroSym) + inline def onInlineCall[T](inlineSym: Symbol)(inline body: T): T = + val event = beforeInlineCall(inlineSym) try body - finally afterMacroSplice(event) - protected def beforeMacroSplice(macroSym: Symbol): TracedEventId = TracedEventId.Empty - protected def afterMacroSplice(event: TracedEventId): Unit = () + finally afterInlineCall(event) + protected def beforeInlineCall(inlineSym: Symbol): TracedEventId = TracedEventId.Empty + protected def afterInlineCall(event: TracedEventId): Unit = () inline def onCompletion[T](root: Symbol, associatedFile: => AbstractFile)(inline body: T): T = val (event, completionName) = beforeCompletion(root, associatedFile) @@ -178,7 +178,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) enum Category: def name: String = this.toString().toLowerCase() - case Run, Phase, File, TypeCheck, Implicit, Macro, Completion + case Run, Phase, File, TypeCheck, Implicit, Inline, Completion private [profile] val chromeTrace = if ctx.settings.YprofileTrace.isDefault then null @@ -317,8 +317,8 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context) override def beforeImplicitSearch(pt: Type): TracedEventId = traceDurationStart(Category.Implicit, s"?[${symbolName(pt.typeSymbol)}]", colour = "yellow") override def afterImplicitSearch(event: TracedEventId): Unit = traceDurationEnd(Category.Implicit, event, colour = "yellow") - override def beforeMacroSplice(macroSym: Symbol): TracedEventId = traceDurationStart(Category.Macro, s"«${symbolName(macroSym)}»", colour = "olive") - override def afterMacroSplice(event: TracedEventId): Unit = traceDurationEnd(Category.Macro, event, colour = "olive") + override def beforeInlineCall(inlineSym: Symbol): TracedEventId = traceDurationStart(Category.Inline, s"«${symbolName(inlineSym)}»", colour = "olive") + override def afterInlineCall(event: TracedEventId): Unit = traceDurationEnd(Category.Inline, event, colour = "olive") override def beforeCompletion(root: Symbol, associatedFile: => AbstractFile): (TracedEventId, String) = if chromeTrace == null diff --git a/compiler/src/dotty/tools/dotc/report.scala b/compiler/src/dotty/tools/dotc/report.scala index 5517bd005077..05020700f11b 100644 --- a/compiler/src/dotty/tools/dotc/report.scala +++ b/compiler/src/dotty/tools/dotc/report.scala @@ -1,13 +1,11 @@ package dotty.tools.dotc -import reporting.* -import Diagnostic.* -import util.{SourcePosition, NoSourcePosition, SrcPos} -import core.* -import Contexts.*, Flags.*, Symbols.*, Decorators.* -import config.SourceVersion import ast.* -import config.Feature.sourceVersion +import core.*, Contexts.*, Flags.*, Symbols.*, Decorators.* +import config.Feature.sourceVersion, config.SourceVersion +import reporting.*, Diagnostic.* +import util.{SourcePosition, NoSourcePosition, SrcPos} + import java.lang.System.currentTimeMillis object report: @@ -23,7 +21,7 @@ object report: ctx.reporter.report(warning) def deprecationWarning(msg: Message, pos: SrcPos, origin: String = "")(using Context): Unit = - issueWarning(new DeprecationWarning(msg, pos.sourcePos, origin)) + issueWarning(DeprecationWarning(msg, addInlineds(pos), origin)) def migrationWarning(msg: Message, pos: SrcPos)(using Context): Unit = issueWarning(new MigrationWarning(msg, pos.sourcePos)) @@ -54,6 +52,9 @@ object report: else issueWarning(new FeatureWarning(msg, pos.sourcePos)) end featureWarning + def warning(msg: Message, pos: SrcPos, origin: String)(using Context): Unit = + issueWarning(LintWarning(msg, addInlineds(pos), origin)) + def warning(msg: Message, pos: SrcPos)(using Context): Unit = issueWarning(new Warning(msg, addInlineds(pos))) @@ -125,7 +126,9 @@ object report: private def addInlineds(pos: SrcPos)(using Context): SourcePosition = def recur(pos: SourcePosition, inlineds: List[Trees.Tree[?]]): SourcePosition = inlineds match - case inlined :: inlineds1 => pos.withOuter(recur(inlined.sourcePos, inlineds1)) + case inlined :: inlineds => + val outer = recur(inlined.sourcePos, inlineds) + pos.withOuter(outer) case Nil => pos recur(pos.sourcePos, tpd.enclosingInlineds) diff --git a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala index 6a2d88f4e82f..20be33716831 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala @@ -8,9 +8,9 @@ import dotty.tools.dotc.config.Settings.Setting import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.interfaces.Diagnostic.{ERROR, INFO, WARNING} import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.util.chaining.* import java.util.{Collections, Optional, List => JList} -import scala.util.chaining.* import core.Decorators.toMessage object Diagnostic: @@ -36,6 +36,18 @@ object Diagnostic: pos: SourcePosition ) extends Error(msg, pos) + /** A Warning with an origin. The semantics of `origin` depend on the warning. + * For example, an unused import warning has an origin that specifies the unused selector. + * The origin of a deprecation is the deprecated element. + */ + trait OriginWarning(val origin: String): + self: Warning => + + /** Lints are likely to be filtered. Provide extra axes for filtering by `-Wconf`. + */ + class LintWarning(msg: Message, pos: SourcePosition, origin: String) + extends Warning(msg, pos), OriginWarning(origin) + class Warning( msg: Message, pos: SourcePosition @@ -73,13 +85,9 @@ object Diagnostic: def enablingOption(using Context): Setting[Boolean] = ctx.settings.unchecked } - class DeprecationWarning( - msg: Message, - pos: SourcePosition, - val origin: String - ) extends ConditionalWarning(msg, pos) { + class DeprecationWarning(msg: Message, pos: SourcePosition, origin: String) + extends ConditionalWarning(msg, pos), OriginWarning(origin): def enablingOption(using Context): Setting[Boolean] = ctx.settings.deprecation - } class MigrationWarning( msg: Message, @@ -104,5 +112,5 @@ class Diagnostic( override def diagnosticRelatedInformation: JList[interfaces.DiagnosticRelatedInformation] = Collections.emptyList() - override def toString: String = s"$getClass at $pos: $message" + override def toString: String = s"$getClass at $pos L${pos.line+1}: $message" end Diagnostic diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index c3cad8cbfea5..7b4dca438bd8 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -220,6 +220,21 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe case DeprecatedInfixNamedArgumentSyntaxID // errorNumber: 204 case GivenSearchPriorityID // errorNumber: 205 case EnumMayNotBeValueClassesID // errorNumber: 206 + case IllegalUnrollPlacementID // errorNumber: 207 - unused in LTS + case ExtensionHasDefaultID // errorNumber: 208 + case FormatInterpolationErrorID // errorNumber: 209 + case ValueClassCannotExtendAliasOfAnyValID // errorNumber: 210 + case MatchIsNotPartialFunctionID // errorNumber: 211 + case OnlyFullyDependentAppliedConstructorTypeID // errorNumber: 212 + case PointlessAppliedConstructorTypeID // errorNumber: 213 + case IllegalContextBoundsID // errorNumber: 214 + case NamedPatternNotApplicableID // errorNumber: 215 + case UnnecessaryNN // errorNumber: 216 + case ErasedNotPureID // errorNumber: 217 + case IllegalErasedDefID // errorNumber: 218 + case CannotInstantiateQuotedTypeVarID // errorNumber: 219 + case DefaultShadowsGivenID // errorNumber: 220 + case RecurseWithDefaultID // errorNumber: 221 def errorNumber = ordinal - 1 diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala b/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala index bb02a08d2e46..e09dd1d6e69e 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageKind.scala @@ -23,6 +23,7 @@ enum MessageKind: case PotentialIssue case UnusedSymbol case Staging + case Interpolation /** Human readable message that will end up being shown to the user. * NOTE: This is only used in the situation where you have multiple words diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 7db5112b6674..45caf480f65e 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -209,20 +209,21 @@ trait MessageRendering { sb.toString } - private def appendFilterHelp(dia: Diagnostic, sb: StringBuilder): Unit = + private def appendFilterHelp(dia: Diagnostic, sb: StringBuilder)(using Context, Level, Offset): Unit = + extension (sb: StringBuilder) def nl: sb.type = sb.append(EOL).append(offsetBox) import dia.msg val hasId = msg.errorId.errorNumber >= 0 val (category, origin) = dia match - case _: UncheckedWarning => ("unchecked", "") + case _: UncheckedWarning => ("unchecked", "") case w: DeprecationWarning => ("deprecation", w.origin) - case _: FeatureWarning => ("feature", "") - case _ => ("", "") + case _: FeatureWarning => ("feature", "") + case _ => ("", "") var entitled = false def addHelp(what: String)(value: String): Unit = if !entitled then - sb.append(EOL).append("Matching filters for @nowarn or -Wconf:") + sb.nl.append("Matching filters for @nowarn or -Wconf:") entitled = true - sb.append(EOL).append(" - ").append(what).append(value) + sb.nl.append(" - ").append(what).append(value) if hasId then addHelp("id=E")(msg.errorId.errorNumber.toString) addHelp("name=")(msg.errorId.productPrefix.stripSuffix("ID")) diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index eb9756028c27..a0f454f1818c 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -95,6 +95,7 @@ abstract class Reporter extends interfaces.ReporterResult { private var _errorCount = 0 private var _warningCount = 0 + private var _infoCount = 0 /** The number of errors reported by this reporter (ignoring outer reporters) */ def errorCount: Int = _errorCount @@ -112,12 +113,17 @@ abstract class Reporter extends interfaces.ReporterResult { private var warnings: List[Warning] = Nil + private var infos: List[Info] = Nil + /** All errors reported by this reporter (ignoring outer reporters) */ def allErrors: List[Error] = errors /** All warnings reported by this reporter (ignoring outer reporters) */ def allWarnings: List[Warning] = warnings + /** All infos reported by this reporter (ignoring outer reporters) */ + def allInfos: List[Info] = infos + /** Were sticky errors reported? Overridden in StoreReporter. */ def hasStickyErrors: Boolean = false @@ -171,7 +177,9 @@ abstract class Reporter extends interfaces.ReporterResult { _errorCount += 1 if ctx.typerState.isGlobalCommittable then ctx.base.errorsToBeReported = true - case _: Info => // nothing to do here + case i: Info => + infos = i :: infos + _infoCount += 1 // match error if d is something else } markReported(dia) diff --git a/compiler/src/dotty/tools/dotc/reporting/WConf.scala b/compiler/src/dotty/tools/dotc/reporting/WConf.scala index 3842067f5390..cff15aa6dc38 100644 --- a/compiler/src/dotty/tools/dotc/reporting/WConf.scala +++ b/compiler/src/dotty/tools/dotc/reporting/WConf.scala @@ -10,15 +10,18 @@ import dotty.tools.dotc.interfaces.SourceFile import dotty.tools.dotc.reporting.MessageFilter.SourcePattern import java.util.regex.PatternSyntaxException +import scala.PartialFunction.cond import scala.annotation.internal.sharable import scala.util.matching.Regex enum MessageFilter: - def matches(message: Diagnostic): Boolean = this match + def matches(message: Diagnostic): Boolean = + import Diagnostic.* + this match case Any => true - case Deprecated => message.isInstanceOf[Diagnostic.DeprecationWarning] - case Feature => message.isInstanceOf[Diagnostic.FeatureWarning] - case Unchecked => message.isInstanceOf[Diagnostic.UncheckedWarning] + case Deprecated => message.isInstanceOf[DeprecationWarning] + case Feature => message.isInstanceOf[FeatureWarning] + case Unchecked => message.isInstanceOf[UncheckedWarning] case MessageID(errorId) => message.msg.errorId == errorId case MessagePattern(pattern) => val noHighlight = message.msg.message.replaceAll("\\e\\[[\\d;]*[^\\d;]","") @@ -31,7 +34,7 @@ enum MessageFilter: pattern.findFirstIn(path).nonEmpty case Origin(pattern) => message match - case message: Diagnostic.DeprecationWarning => pattern.findFirstIn(message.origin).nonEmpty + case message: OriginWarning => pattern.findFirstIn(message.origin).nonEmpty case _ => false case None => false @@ -56,12 +59,12 @@ object WConf: private type Conf = (List[MessageFilter], Action) def parseAction(s: String): Either[List[String], Action] = s match - case "error" | "e" => Right(Error) - case "warning" | "w" => Right(Warning) - case "verbose" | "v" => Right(Verbose) - case "info" | "i" => Right(Info) - case "silent" | "s" => Right(Silent) - case _ => Left(List(s"unknown action: `$s`")) + case "error" | "e" => Right(Error) + case "warning" | "w" => Right(Warning) + case "verbose" | "v" => Right(Verbose) + case "info" | "i" => Right(Info) + case "silent" | "s" => Right(Silent) + case _ => Left(List(s"unknown action: `$s`")) private def regex(s: String) = try Right(s.r) @@ -134,11 +137,20 @@ object WConf: if (parseErrorss.nonEmpty) Left(parseErrorss.flatten) else Right(WConf(configs)) -class Suppression(val annotPos: SourcePosition, filters: List[MessageFilter], val start: Int, end: Int, val verbose: Boolean): - private[this] var _used = false - def used: Boolean = _used - def markUsed(): Unit = { _used = true } - +class Suppression(val annotPos: SourcePosition, val filters: List[MessageFilter], val start: Int, val end: Int, val verbose: Boolean): + inline def unusedState = 0 + inline def usedState = 1 + inline def supersededState = 2 + private var _used = unusedState + def used: Boolean = _used == usedState + def superseded: Boolean = _used == supersededState + def markUsed(): Unit = + _used = usedState + def markSuperseded(): Unit = + _used = supersededState def matches(dia: Diagnostic): Boolean = val pos = dia.pos pos.exists && start <= pos.start && pos.end <= end && filters.forall(_.matches(dia)) + + override def toString = s"Suppress in ${annotPos.source} $start..$end [${filters.mkString(", ")}]" +end Suppression diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index f9210845bd54..0e2d64266dbe 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -60,7 +60,7 @@ trait ShowMatchTrace(tps: Type*)(using Context) extends Message: override def msgPostscript(using Context): String = super.msgPostscript ++ matchReductionAddendum(tps*) -abstract class TypeMismatchMsg(found: Type, expected: Type)(errorId: ErrorMessageID)(using Context) +abstract class TypeMismatchMsg(found: Type, val expected: Type)(errorId: ErrorMessageID)(using Context) extends Message(errorId), ShowMatchTrace(found, expected): def kind = MessageKind.TypeMismatch def explain(using Context) = err.whyNoMatchStr(found, expected) @@ -352,7 +352,7 @@ class TypeMismatch(val found: Type, expected: Type, val inTree: Option[untpd.Tre ++ addenda.dropWhile(_.isEmpty).headOption.getOrElse(importSuggestions) override def explain(using Context) = - val treeStr = inTree.map(x => s"\nTree: ${x.show}").getOrElse("") + val treeStr = inTree.map(x => s"\nTree:\n\n${x.show}\n").getOrElse("") treeStr + "\n" + super.explain end TypeMismatch @@ -1793,6 +1793,12 @@ class ValueClassParameterMayNotBeCallByName(valueClass: Symbol, param: Symbol)(u def explain(using Context) = "" } +class ValueClassCannotExtendAliasOfAnyVal(valueClass: Symbol, alias: Symbol)(using Context) + extends SyntaxMsg(ValueClassCannotExtendAliasOfAnyValID) { + def msg(using Context) = i"""A value class cannot extend a type alias ($alias) of ${hl("AnyVal")}""" + def explain(using Context) = "" +} + class SuperCallsNotAllowedInlineable(symbol: Symbol)(using Context) extends SyntaxMsg(SuperCallsNotAllowedInlineableID) { def msg(using Context) = i"Super call not allowed in inlineable $symbol" @@ -2295,12 +2301,16 @@ class JavaSymbolIsNotAValue(symbol: Symbol)(using Context) extends TypeMsg(JavaS class DoubleDefinition(decl: Symbol, previousDecl: Symbol, base: Symbol)(using Context) extends NamingMsg(DoubleDefinitionID) { + import Signature.MatchDegree.* + + private def erasedType: Type = + if ctx.erasedTypes then decl.info + else TypeErasure.transformInfo(decl, decl.info) + def msg(using Context) = { def nameAnd = if (decl.name != previousDecl.name) " name and" else "" - def erasedType = if ctx.erasedTypes then i" ${decl.info}" else "" def details(using Context): String = if (decl.isRealMethod && previousDecl.isRealMethod) { - import Signature.MatchDegree.* // compare the signatures when both symbols represent methods decl.signature.matchDegree(previousDecl.signature) match { @@ -2325,7 +2335,7 @@ extends NamingMsg(DoubleDefinitionID) { |Consider adding a @targetName annotation to one of the conflicting definitions |for disambiguation.""" else "" - i"have the same$nameAnd type$erasedType after erasure.$hint" + i"have the same$nameAnd type $erasedType after erasure.$hint" } } else "" @@ -2338,7 +2348,7 @@ extends NamingMsg(DoubleDefinitionID) { } val clashDescription = if (decl.owner eq previousDecl.owner) - "Double definition" + "Conflicting definitions" else if ((decl.owner eq base) || (previousDecl eq base)) "Name clash between defined and inherited member" else @@ -2351,7 +2361,43 @@ extends NamingMsg(DoubleDefinitionID) { |""" } + details } - def explain(using Context) = "" + def explain(using Context) = + decl.signature.matchDegree(previousDecl.signature) match + case FullMatch => + i""" + |As part of the Scala compilation pipeline every type is reduced to its erased + |(runtime) form. In this phase, among other transformations, generic parameters + |disappear and separate parameter-list boundaries are flattened. + | + |For example, both `f[T](x: T)(y: String): Unit` and `f(x: Any, z: String): Unit` + |erase to the same runtime signature `f(x: Object, y: String): Unit`. Note that + |parameter names are irrelevant. + | + |In your code the two declarations + | + | ${previousDecl.showDcl} + | ${decl.showDcl} + | + |erase to the identical signature + | + | ${erasedType} + | + |so the compiler cannot keep both: the generated bytecode symbols would collide. + | + |To fix this error, you need to disambiguate the two definitions. You can either: + | + |1. Rename one of the definitions, or + |2. Keep the same names in source but give one definition a distinct + | bytecode-level name via `@targetName` for example: + | + | @targetName("${decl.name.show}_2") + | ${decl.showDcl} + | + |Choose the `@targetName` argument carefully: it is the name that will be used + |when calling the method externally, so it should be unique and descriptive. + """ + case _ => "" + } class ImportRenamedTwice(ident: untpd.Ident)(using Context) extends SyntaxMsg(ImportRenamedTwiceID) { @@ -2474,6 +2520,17 @@ class ExtensionNullifiedByMember(method: Symbol, target: Symbol)(using Context) | |The extension may be invoked as though selected from an arbitrary type if conversions are in play.""" +class ExtensionHasDefault(method: Symbol)(using Context) + extends Message(ExtensionHasDefaultID): + def kind = MessageKind.PotentialIssue + def msg(using Context) = + i"""Extension method ${hl(method.name.toString)} should not have a default argument for its receiver.""" + def explain(using Context) = + i"""The receiver cannot be omitted when an extension method is invoked as a selection. + |A default argument for that parameter would never be used in that case. + |An extension method can be invoked as a regular method, but if that is the intended usage, + |it should not be defined as an extension.""" + class TraitCompanionWithMutableStatic()(using Context) extends SyntaxMsg(TraitCompanionWithMutableStaticID) { def msg(using Context) = i"Companion of traits cannot define mutable @static fields" @@ -3081,7 +3138,7 @@ extends ReferenceMsg(CannotBeAccessedID): case _ => i"none of the overloaded alternatives named $name can" val where = if (ctx.owner.exists) i" from ${ctx.owner.enclosingClass}" else "" - val whyNot = new StringBuffer + val whyNot = new StringBuilder for alt <- alts do val cls = alt.owner.enclosingSubClass val owner = if cls.exists then cls else alt.owner @@ -3089,10 +3146,10 @@ extends ReferenceMsg(CannotBeAccessedID): if alt.is(Protected) then if alt.privateWithin.exists && alt.privateWithin != owner then if owner.is(Final) then alt.privateWithin.showLocated - else alt.privateWithin.showLocated + ", or " + owner.showLocated + " or one of its subclasses" + else s"${alt.privateWithin.showLocated}, or ${owner.showLocated} or one of its subclasses" else if owner.is(Final) then owner.showLocated - else owner.showLocated + " or one of its subclasses" + else s"${owner.showLocated} or one of its subclasses" else alt.privateWithin.orElse(owner).showLocated val accessMod = if alt.is(Protected) then "protected" else "private" @@ -3155,22 +3212,31 @@ extends SyntaxMsg(VolatileOnValID): protected def msg(using Context): String = "values cannot be volatile" protected def explain(using Context): String = "" -class UnusedSymbol(errorText: String)(using Context) -extends Message(UnusedSymbolID) { + +class UnusedSymbol(errorText: String, val actions: List[CodeAction] = Nil)(using Context) +extends Message(UnusedSymbolID): def kind = MessageKind.UnusedSymbol override def msg(using Context) = errorText override def explain(using Context) = "" -} - -object UnusedSymbol { - def imports(using Context): UnusedSymbol = new UnusedSymbol(i"unused import") - def localDefs(using Context): UnusedSymbol = new UnusedSymbol(i"unused local definition") - def explicitParams(using Context): UnusedSymbol = new UnusedSymbol(i"unused explicit parameter") - def implicitParams(using Context): UnusedSymbol = new UnusedSymbol(i"unused implicit parameter") - def privateMembers(using Context): UnusedSymbol = new UnusedSymbol(i"unused private member") - def patVars(using Context): UnusedSymbol = new UnusedSymbol(i"unused pattern variable") -} + override def actions(using Context) = this.actions + +object UnusedSymbol: + def imports(actions: List[CodeAction])(using Context): UnusedSymbol = UnusedSymbol(i"unused import", actions) + def localDefs(using Context): UnusedSymbol = UnusedSymbol(i"unused local definition") + def explicitParams(sym: Symbol)(using Context): UnusedSymbol = + UnusedSymbol(i"unused explicit parameter${paramAddendum(sym)}") + def implicitParams(sym: Symbol)(using Context): UnusedSymbol = + UnusedSymbol(i"unused implicit parameter${paramAddendum(sym)}") + def privateMembers(using Context): UnusedSymbol = UnusedSymbol(i"unused private member") + def patVars(using Context): UnusedSymbol = UnusedSymbol(i"unused pattern variable") + def unsetLocals(using Context): UnusedSymbol = + UnusedSymbol(i"unset local variable, consider using an immutable val instead") + def unsetPrivates(using Context): UnusedSymbol = + UnusedSymbol(i"unset private variable, consider using an immutable val instead") + private def paramAddendum(sym: Symbol)(using Context): String = + if sym.denot.owner.is(ExtensionMethod) then i" in extension ${sym.denot.owner}" + else "" final class QuotedTypeMissing(tpe: Type)(using Context) extends StagingMessage(QuotedTypeMissingID): @@ -3194,3 +3260,62 @@ final class EnumMayNotBeValueClasses(sym: Symbol)(using Context) extends SyntaxM def explain(using Context) = "" end EnumMayNotBeValueClasses + +class BadFormatInterpolation(errorText: String)(using Context) extends Message(FormatInterpolationErrorID): + def kind = MessageKind.Interpolation + protected def msg(using Context) = errorText + protected def explain(using Context) = "" + +class MatchIsNotPartialFunction(using Context) extends SyntaxMsg(MatchIsNotPartialFunctionID): + protected def msg(using Context) = + "match expression in result of block will not be used to synthesize partial function" + protected def explain(using Context) = + i"""A `PartialFunction` can be synthesized from a function literal if its body is just a pattern match. + | + |For example, `collect` takes a `PartialFunction`. + | (1 to 10).collect(i => i match { case n if n % 2 == 0 => n }) + |is equivalent to using a "pattern-matching anonymous function" directly: + | (1 to 10).collect { case n if n % 2 == 0 => n } + |Compare an operation that requires a `Function1` instead: + | (1 to 10).map { case n if n % 2 == 0 => n case n => n + 1 } + | + |As a convenience, the "selector expression" of the match can be an arbitrary expression: + | List("1", "two", "3").collect(x => Try(x.toInt) match { case Success(i) => i }) + |In this example, `isDefinedAt` evaluates the selector expression and any guard expressions + |in the pattern match in order to report whether an input is in the domain of the function. + | + |However, blocks of statements are not supported by this idiom: + | List("1", "two", "3").collect: x => + | val maybe = Try(x.toInt) // statements preceding the match + | maybe match + | case Success(i) if i % 2 == 0 => i // throws MatchError on cases not covered + | + |This restriction is enforced to simplify the evaluation semantics of the partial function. + |Otherwise, it might not be clear what is computed by `isDefinedAt`. + | + |Efficient operations will use `applyOrElse` to avoid computing the match twice, + |but the `apply` body would be executed "per element" in the example.""" + +final class IllegalContextBounds(using Context) extends SyntaxMsg(IllegalContextBoundsID): + override protected def msg(using Context): String = + i"Context bounds are not allowed in this position" + + override protected def explain(using Context): String = "" + +final class NamedPatternNotApplicable(selectorType: Type)(using Context) extends PatternMatchMsg(NamedPatternNotApplicableID): + override protected def msg(using Context): String = + i"Named patterns cannot be used with $selectorType, because it is not a named tuple or case class" + + override protected def explain(using Context): String = "" + +final class DefaultShadowsGiven(name: Name)(using Context) extends TypeMsg(DefaultShadowsGivenID): + override protected def msg(using Context): String = + i"Argument for implicit parameter $name was supplied using a default argument." + override protected def explain(using Context): String = + "Usually the given in scope is intended, but you must specify it after explicit `using`." + +final class RecurseWithDefault(name: Name)(using Context) extends TypeMsg(RecurseWithDefaultID): + override protected def msg(using Context): String = + i"Recursive call used a default argument for parameter $name." + override protected def explain(using Context): String = + "It's more explicit to pass current or modified arguments in a recursion." \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 73f69dd6f2ad..59697796b76d 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -24,7 +24,7 @@ import java.io.PrintWriter import scala.collection.mutable import scala.util.hashing.MurmurHash3 -import scala.util.chaining.* +import dotty.tools.dotc.util.chaining.* /** This phase sends a representation of the API of classes to sbt via callbacks. * diff --git a/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala b/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala index b4a2067e5c43..d39d3d095bb7 100644 --- a/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala +++ b/compiler/src/dotty/tools/dotc/semanticdb/ExtractSemanticDB.scala @@ -63,7 +63,7 @@ class ExtractSemanticDB private (phaseMode: ExtractSemanticDB.PhaseMode) extends private def computeDiagnostics( sourceRoot: String, - warnings: Map[SourceFile, List[Warning]], + warnings: Map[SourceFile, List[dotty.tools.dotc.reporting.Diagnostic]], append: ((Path, List[Diagnostic])) => Unit)(using Context): Boolean = monitor(phaseName) { val unit = ctx.compilationUnit warnings.get(unit.source).foreach { ws => @@ -104,14 +104,14 @@ class ExtractSemanticDB private (phaseMode: ExtractSemanticDB.PhaseMode) extends val appendDiagnostics = phaseMode == ExtractSemanticDB.PhaseMode.AppendDiagnostics val unitContexts = units.map(ctx.fresh.setCompilationUnit(_).withRootImports) if (appendDiagnostics) - val warnings = ctx.reporter.allWarnings.groupBy(w => w.pos.source) + val warningsAndInfos = (ctx.reporter.allWarnings ++ ctx.reporter.allInfos).groupBy(w => w.pos.source) val buf = mutable.ListBuffer.empty[(Path, Seq[Diagnostic])] val units0 = - for unitCtx <- unitContexts if computeDiagnostics(sourceRoot, warnings, buf += _)(using unitCtx) + for unitCtx <- unitContexts if computeDiagnostics(sourceRoot, warningsAndInfos, buf += _)(using unitCtx) yield unitCtx.compilationUnit cancellable { - buf.toList.asJava.parallelStream().forEach { case (out, warnings) => - ExtractSemanticDB.appendDiagnostics(warnings, out) + buf.toList.asJava.parallelStream().forEach { case (out, diagnostics) => + ExtractSemanticDB.appendDiagnostics(diagnostics, out) } } units0 @@ -320,7 +320,7 @@ object ExtractSemanticDB: registerDefinition(tree.symbol, selectSpan(tree), Set.empty, tree.source) case tree => registerDefinition(tree.symbol, tree.span, Set.empty, tree.source) case tree: NamedDefTree => - if !tree.symbol.isAllOf(ModuleValCreationFlags) then + if tree.symbol.exists && !tree.symbol.isAllOf(ModuleValCreationFlags) then tree match { case tree: ValDef if tree.symbol.isAllOf(EnumValue) => tree.rhs match @@ -379,7 +379,7 @@ object ExtractSemanticDB: traverseAnnotsOfDefinition(ctorSym) ctorParams(tree.constr.termParamss, tree.constr.leadingTypeParams, tree.body) registerDefinition(ctorSym, tree.constr.nameSpan.startPos, Set.empty, tree.source) - case tree: Apply => + case tree: Apply if tree.fun.symbol.exists => @tu lazy val genParamSymbol: Name => String = tree.fun.symbol.funParamSymbol traverse(tree.fun) synth.tryFindSynthetic(tree).foreach(synthetics.addOne) diff --git a/compiler/src/dotty/tools/dotc/semanticdb/SemanticSymbolBuilder.scala b/compiler/src/dotty/tools/dotc/semanticdb/SemanticSymbolBuilder.scala index 81f5d37f443f..8e7e0ccc2c4b 100644 --- a/compiler/src/dotty/tools/dotc/semanticdb/SemanticSymbolBuilder.scala +++ b/compiler/src/dotty/tools/dotc/semanticdb/SemanticSymbolBuilder.scala @@ -108,10 +108,12 @@ class SemanticSymbolBuilder: else if (sym.isScala2PackageObject) then b.append(Symbols.PackageObjectDescriptor) else + def isScalaMethodOrVar = sym.isOneOf(Method | Mutable) && !sym.is(JavaDefined) + def isJavaMethod = sym.is(Method) && sym.is(JavaDefined) addName(b, sym.name) if sym.is(Package) then b.append('/') else if sym.isType || sym.isAllOf(JavaModule) then b.append('#') - else if sym.isOneOf(Method | Mutable) + else if (isScalaMethodOrVar || isJavaMethod) && (!sym.is(StableRealizable) || sym.isConstructor) then b.append('('); addOverloadIdx(sym); b.append(").") else b.append('.') diff --git a/compiler/src/dotty/tools/dotc/semanticdb/SyntheticsExtractor.scala b/compiler/src/dotty/tools/dotc/semanticdb/SyntheticsExtractor.scala index af38315a857e..6bf3b0468ffc 100644 --- a/compiler/src/dotty/tools/dotc/semanticdb/SyntheticsExtractor.scala +++ b/compiler/src/dotty/tools/dotc/semanticdb/SyntheticsExtractor.scala @@ -76,7 +76,9 @@ class SyntheticsExtractor: ) ).toOpt - case tree: Apply if tree.fun.symbol.is(Implicit) => + case tree: Apply + if tree.fun.symbol.is(Implicit) || + (tree.fun.symbol.name == nme.apply && tree.fun.span.isSynthetic) => val pos = range(tree.span, tree.source) s.Synthetic( pos, diff --git a/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala b/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala index 4293ecd6ca43..11467e216aba 100644 --- a/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/semanticdb/TypeOps.scala @@ -9,13 +9,13 @@ import core.Annotations.Annotation import core.Flags import core.Names.Name import core.StdNames.tpnme -import scala.util.chaining.scalaUtilChainingOps import collection.mutable import dotty.tools.dotc.{semanticdb => s} import Scala3.{FakeSymbol, SemanticSymbol, WildcardTypeSymbol, TypeParamRefSymbol, TermParamRefSymbol, RefinementSymbol} import dotty.tools.dotc.core.Names.Designator +import dotty.tools.dotc.util.chaining.* class TypeOps: import SymbolScopeOps.* @@ -96,7 +96,11 @@ class TypeOps: // We try to find the "actual" binder of : `inner`, // and register them to the symbol table with `(, inner) -> ` // instead of `("y", outer) -> ` - if lam.paramNames.contains(sym.name) then + // We must also check for parameter shadowing such as def shadowParam(x: Int) = {val x = true} + // We skip param symbol check if owner is not a LambdaType for proper MatchType paramRef entry in the paramRefSymtab + // for more information: https://github.com/scala/scala3/pull/23161#discussion_r2097755983 + + if (sym.is(Flags.Param) || !sym.owner.info.isInstanceOf[LambdaType]) && lam.paramNames.contains(sym.name) then paramRefSymtab((lam, sym.name)) = sym else enterParamRef(lam.resType) diff --git a/compiler/src/dotty/tools/dotc/staging/CrossStageSafety.scala b/compiler/src/dotty/tools/dotc/staging/CrossStageSafety.scala index 1832eeabca7e..4534af0f0883 100644 --- a/compiler/src/dotty/tools/dotc/staging/CrossStageSafety.scala +++ b/compiler/src/dotty/tools/dotc/staging/CrossStageSafety.scala @@ -189,11 +189,19 @@ class CrossStageSafety extends TreeMapWithStages { /** Check level consistency of terms references */ private def checkLevelConsistency(tree: Ident | This)(using Context): Unit = + def isStatic(pre: Type)(using Context): Boolean = pre match + case pre: NamedType => + val sym = pre.currentSymbol + sym.is(Package) || sym.isStaticOwner && isStatic(pre.prefix) + case pre: ThisType => isStatic(pre.tref) + case _ => true new TypeTraverser { def traverse(tp: Type): Unit = tp match case tp @ TermRef(NoPrefix, _) if !tp.symbol.isStatic && level != levelOf(tp.symbol) => levelError(tp.symbol, tp, tree.srcPos) + case tp: ThisType if isStatic(tp) => + // static object (OK) case tp: ThisType if level != -1 && level != levelOf(tp.cls) => levelError(tp.cls, tp, tree.srcPos) case tp: AnnotatedType => @@ -201,7 +209,7 @@ class CrossStageSafety extends TreeMapWithStages { case _ if tp.typeSymbol.is(Package) => // OK case _ => - traverseChildren(tp) + traverseChildren(tp) }.traverse(tree.tpe) private def levelError(sym: Symbol, tp: Type, pos: SrcPos)(using Context): tp.type = { diff --git a/compiler/src/dotty/tools/dotc/staging/HealType.scala b/compiler/src/dotty/tools/dotc/staging/HealType.scala index a73f884fbac9..509049e131c8 100644 --- a/compiler/src/dotty/tools/dotc/staging/HealType.scala +++ b/compiler/src/dotty/tools/dotc/staging/HealType.scala @@ -76,7 +76,7 @@ class HealType(pos: SrcPos)(using Context) extends TypeMap { tp match case tp @ NamedType(NoPrefix, _) if level > levelOf(tp.symbol) => tp.symbol case tp: NamedType if !tp.symbol.isStatic => levelInconsistentRootOfPath(tp.prefix) - case tp: ThisType if level > levelOf(tp.cls) => tp.cls + case tp: ThisType if level > levelOf(tp.cls) && !tp.cls.isRefinementClass => tp.cls case _ => NoSymbol /** Try to heal reference to type `T` used in a higher level than its definition. diff --git a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala index 237db90b315f..ee4055bdafb1 100644 --- a/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala +++ b/compiler/src/dotty/tools/dotc/transform/AccessProxies.scala @@ -136,8 +136,13 @@ abstract class AccessProxies { if accessorClass.is(Package) then accessorClass = ctx.owner.topLevelClass val accessorName = accessorNameOf(accessed.name, accessorClass) + val mappedInfo = accessed.info match + // TypeRef pointing to module class seems to not be stable, so we remap that to a TermRef + // see test i22593.scala (and issue #i22593) + case tref @ TypeRef(prefix, _) if tref.symbol.is(Module) => TermRef(prefix, tref.symbol.companionModule) + case other => other val accessorInfo = - accessed.info.ensureMethodic.asSeenFrom(accessorClass.thisType, accessed.owner) + mappedInfo.ensureMethodic.asSeenFrom(accessorClass.thisType, accessed.owner) val accessor = accessorSymbol(accessorClass, accessorName, accessorInfo, accessed) rewire(reference, accessor) } diff --git a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala index a85cabdd5460..7ecdf79af984 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckShadowing.scala @@ -49,26 +49,22 @@ class CheckShadowing extends MiniPhase: override def description: String = CheckShadowing.description + override def isEnabled(using Context): Boolean = ctx.settings.Xlint.value.nonEmpty + override def isRunnable(using Context): Boolean = - super.isRunnable && - ctx.settings.Xlint.value.nonEmpty && - !ctx.isJava + super.isRunnable && ctx.settings.Xlint.value.nonEmpty && !ctx.isJava - // Setup before the traversal override def prepareForUnit(tree: tpd.Tree)(using Context): Context = val data = ShadowingData() val fresh = ctx.fresh.setProperty(_key, data) shadowingDataApply(sd => sd.registerRootImports())(using fresh) - // Reporting on traversal's end override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = shadowingDataApply(sd => reportShadowing(sd.getShadowingResult) ) tree - // MiniPhase traversal : - override def prepareForPackageDef(tree: tpd.PackageDef)(using Context): Context = shadowingDataApply(sd => sd.inNewScope()) ctx @@ -91,16 +87,16 @@ class CheckShadowing extends MiniPhase: ) override def prepareForTypeDef(tree: tpd.TypeDef)(using Context): Context = - if tree.symbol.isAliasType then // if alias, the parent is the current symbol - nestedTypeTraverser(tree.symbol).traverse(tree.rhs) - if tree.symbol.is(Param) then // if param, the parent is up - val owner = tree.symbol.owner + val sym = tree.symbol + if sym.isAliasType then // if alias, the parent is the current symbol + nestedTypeTraverser(sym).traverse(tree.rhs) + if sym.is(Param) then // if param, the parent is up + val owner = sym.owner val parent = if (owner.isConstructor) then owner.owner else owner nestedTypeTraverser(parent).traverse(tree.rhs)(using ctx.outer) - shadowingDataApply(sd => sd.registerCandidate(parent, tree)) - else - ctx - + if isValidTypeParamOwner(sym.owner) then + shadowingDataApply(sd => sd.registerCandidate(parent, tree)) + ctx override def transformPackageDef(tree: tpd.PackageDef)(using Context): tpd.Tree = shadowingDataApply(sd => sd.outOfScope()) @@ -115,13 +111,14 @@ class CheckShadowing extends MiniPhase: tree override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree = - if tree.symbol.is(Param) && isValidTypeParamOwner(tree.symbol.owner) then // Do not register for constructors the work is done for the Class owned equivalent TypeDef + // Do not register for constructors the work is done for the Class owned equivalent TypeDef + if tree.symbol.is(Param) && isValidTypeParamOwner(tree.symbol.owner) then shadowingDataApply(sd => sd.computeTypeParamShadowsFor(tree.symbol.owner)(using ctx.outer)) - if tree.symbol.isAliasType then // No need to start outer here, because the TypeDef reached here it's already the parent + // No need to start outer here, because the TypeDef reached here it's already the parent + if tree.symbol.isAliasType then shadowingDataApply(sd => sd.computeTypeParamShadowsFor(tree.symbol)(using ctx)) tree - // Helpers : private def isValidTypeParamOwner(owner: Symbol)(using Context): Boolean = !owner.isConstructor && !owner.is(Synthetic) && !owner.is(Exported) @@ -141,7 +138,7 @@ class CheckShadowing extends MiniPhase: override def traverse(tree: tpd.Tree)(using Context): Unit = tree match - case t:tpd.TypeDef => + case t: tpd.TypeDef => val newCtx = shadowingDataApply(sd => sd.registerCandidate(parent, t) ) @@ -157,7 +154,7 @@ class CheckShadowing extends MiniPhase: override def traverse(tree: tpd.Tree)(using Context): Unit = tree match - case t:tpd.Import => + case t: tpd.Import => val newCtx = shadowingDataApply(sd => sd.registerImport(t)) traverseChildren(tree)(using newCtx) case _ => @@ -240,7 +237,7 @@ object CheckShadowing: val declarationScope = ctx.effectiveScope val res = declarationScope.lookup(symbol.name) res match - case s: Symbol if s.isType => Some(s) + case s: Symbol if s.isType && s != symbol => Some(s) case _ => lookForUnitShadowedType(symbol)(using ctx.outer) /** Register if the valDef is a private declaration that shadows an inherited field */ @@ -310,4 +307,3 @@ object CheckShadowing: case class ShadowResult(warnings: List[ShadowWarning]) end CheckShadowing - diff --git a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala index 6e626fc5dd9e..efc5e829a6c3 100644 --- a/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala +++ b/compiler/src/dotty/tools/dotc/transform/CheckUnused.scala @@ -1,883 +1,1040 @@ package dotty.tools.dotc.transform -import scala.annotation.tailrec -import scala.collection.mutable - -import dotty.tools.uncheckedNN -import dotty.tools.dotc.ast.tpd -import dotty.tools.dotc.core.Symbols.* -import dotty.tools.dotc.ast.tpd.{Inlined, TreeTraverser} -import dotty.tools.dotc.ast.untpd -import dotty.tools.dotc.ast.untpd.ImportSelector +import dotty.tools.dotc.ast.desugar.{ForArtifact, PatternVar} +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.ast.untpd, untpd.ImportSelector import dotty.tools.dotc.config.ScalaSettings import dotty.tools.dotc.core.Contexts.* -import dotty.tools.dotc.core.Decorators.{em, i} -import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.* -import dotty.tools.dotc.core.Phases.Phase -import dotty.tools.dotc.core.StdNames +import dotty.tools.dotc.core.Names.{Name, SimpleName, DerivedName, TermName, termName} +import dotty.tools.dotc.core.NameOps.{isAnonymousFunctionName, isReplWrapperName, isContextFunction, setterName} +import dotty.tools.dotc.core.NameKinds.{ + BodyRetainerName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols.{ClassSymbol, NoSymbol, Symbol, defn, isDeprecated, requiredClass, requiredModule} +import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.report -import dotty.tools.dotc.reporting.Message -import dotty.tools.dotc.reporting.UnusedSymbol as UnusedSymbolMessage -import dotty.tools.dotc.typer.ImportInfo -import dotty.tools.dotc.util.{Property, SrcPos} -import dotty.tools.dotc.core.Mode -import dotty.tools.dotc.core.Types.{AnnotatedType, ConstantType, NoType, TermRef, Type, TypeTraverser} -import dotty.tools.dotc.core.Flags.flagsString -import dotty.tools.dotc.core.Flags -import dotty.tools.dotc.core.Names.{Name, TermName} -import dotty.tools.dotc.core.NameOps.isReplWrapperName +import dotty.tools.dotc.reporting.{CodeAction, UnusedSymbol} +import dotty.tools.dotc.rewrites.Rewrites import dotty.tools.dotc.transform.MegaPhase.MiniPhase -import dotty.tools.dotc.core.Annotations -import dotty.tools.dotc.core.Definitions -import dotty.tools.dotc.core.NameKinds.WildcardParamName -import dotty.tools.dotc.core.Symbols.Symbol -import dotty.tools.dotc.core.StdNames.nme -import dotty.tools.dotc.util.Spans.Span -import scala.math.Ordering +import dotty.tools.dotc.typer.{ImportInfo, Typer} +import dotty.tools.dotc.typer.Deriving.OriginalTypeClass +import dotty.tools.dotc.util.{Property, Spans, SrcPos}, Spans.Span +import dotty.tools.dotc.util.Chars.{isLineBreakChar, isWhitespace} +import dotty.tools.dotc.util.chaining.* +import java.util.IdentityHashMap -/** - * A compiler phase that checks for unused imports or definitions - * - * Basically, it gathers definition/imports and their usage. If a - * definition/imports does not have any usage, then it is reported. - */ -class CheckUnused private (phaseMode: CheckUnused.PhaseMode, suffix: String, _key: Property.Key[CheckUnused.UnusedData]) extends MiniPhase: - import CheckUnused.* - import UnusedData.* - - private inline def unusedDataApply[U](inline f: UnusedData => U)(using Context): Context = - ctx.property(_key) match - case Some(ud) => f(ud) - case None => () - ctx +import scala.collection.mutable, mutable.{ArrayBuilder, ListBuffer, Stack} - override def phaseName: String = CheckUnused.phaseNamePrefix + suffix +import CheckUnused.* - override def description: String = CheckUnused.description +/** A compiler phase that checks for unused imports or definitions. + */ +class CheckUnused private (phaseMode: PhaseMode, suffix: String) extends MiniPhase: - override def isRunnable(using Context): Boolean = - super.isRunnable && - ctx.settings.WunusedHas.any && - !ctx.isJava + override def phaseName: String = s"checkUnused$suffix" - // ========== SETUP ============ + override def description: String = "check for unused elements" - override def prepareForUnit(tree: tpd.Tree)(using Context): Context = - val data = UnusedData() - tree.getAttachment(_key).foreach(oldData => - data.unusedAggregate = oldData.unusedAggregate - ) - val fresh = ctx.fresh.setProperty(_key, data) - tree.putAttachment(_key, data) - fresh + override def isEnabled(using Context): Boolean = ctx.settings.WunusedHas.any - // ========== END + REPORTING ========== + override def isRunnable(using Context): Boolean = super.isRunnable && ctx.settings.WunusedHas.any && !ctx.isJava - override def transformUnit(tree: tpd.Tree)(using Context): tpd.Tree = - unusedDataApply { ud => - ud.finishAggregation() - if(phaseMode == PhaseMode.Report) then - ud.unusedAggregate.foreach(reportUnused) - } + override def prepareForUnit(tree: Tree)(using Context): Context = + val infos = tree.getAttachment(refInfosKey).getOrElse: + RefInfos().tap(tree.withAttachment(refInfosKey, _)) + ctx.fresh.setProperty(refInfosKey, infos) + override def transformUnit(tree: Tree)(using Context): tree.type = + if phaseMode == PhaseMode.Report then + reportUnused() + tree.removeAttachment(refInfosKey) tree - // ========== MiniPhase Prepare ========== - override def prepareForOther(tree: tpd.Tree)(using Context): Context = - // A standard tree traverser covers cases not handled by the Mega/MiniPhase - traverser.traverse(tree) - ctx - - override def prepareForInlined(tree: tpd.Inlined)(using Context): Context = - traverser.traverse(tree.call) - ctx - - override def prepareForIdent(tree: tpd.Ident)(using Context): Context = + override def transformIdent(tree: Ident)(using Context): tree.type = + refInfos.isAssignment = tree.hasAttachment(AssignmentTarget) if tree.symbol.exists then - unusedDataApply { ud => - @tailrec - def loopOnNormalizedPrefixes(prefix: Type, depth: Int): Unit = - // limit to 10 as failsafe for the odd case where there is an infinite cycle - if depth < 10 && prefix.exists then - ud.registerUsed(prefix.classSymbol, None) - loopOnNormalizedPrefixes(prefix.normalizedPrefix, depth + 1) - - loopOnNormalizedPrefixes(tree.typeOpt.normalizedPrefix, depth = 0) - ud.registerUsed(tree.symbol, Some(tree.name)) - } + // if in an inline expansion, resolve at summonInline (synthetic pos) or in an enclosing call site + val resolving = + refInfos.inlined.isEmpty + || tree.srcPos.isZeroExtentSynthetic + || refInfos.inlined.exists(_.sourcePos.contains(tree.srcPos.sourcePos)) + if resolving && !ignoreTree(tree) then + def loopOverPrefixes(prefix: Type, depth: Int): Unit = + if depth < 10 && prefix.exists && !prefix.classSymbol.isEffectiveRoot then + resolveUsage(prefix.classSymbol, nme.NO_NAME, NoPrefix) + loopOverPrefixes(prefix.normalizedPrefix, depth + 1) + if tree.srcPos.isZeroExtentSynthetic then + loopOverPrefixes(tree.typeOpt.normalizedPrefix, depth = 0) + resolveUsage(tree.symbol, tree.name, tree.typeOpt.importPrefix.skipPackageObject) else if tree.hasType then - unusedDataApply(_.registerUsed(tree.tpe.classSymbol, Some(tree.name))) - else - ctx - - override def prepareForSelect(tree: tpd.Select)(using Context): Context = - val name = tree.removeAttachment(OriginalName) - unusedDataApply(_.registerUsed(tree.symbol, name, includeForImport = tree.qualifier.span.isSynthetic)) - - override def prepareForBlock(tree: tpd.Block)(using Context): Context = - pushInBlockTemplatePackageDef(tree) - - override def prepareForTemplate(tree: tpd.Template)(using Context): Context = - pushInBlockTemplatePackageDef(tree) - - override def prepareForPackageDef(tree: tpd.PackageDef)(using Context): Context = - pushInBlockTemplatePackageDef(tree) - - override def prepareForValDef(tree: tpd.ValDef)(using Context): Context = - unusedDataApply{ud => - // do not register the ValDef generated for `object` - traverseAnnotations(tree.symbol) - if !tree.symbol.is(Module) then - ud.registerDef(tree) - if tree.name.startsWith("derived$") && tree.typeOpt != NoType then - ud.registerUsed(tree.typeOpt.typeSymbol, None, isDerived = true) - ud.addIgnoredUsage(tree.symbol) - } - - override def prepareForDefDef(tree: tpd.DefDef)(using Context): Context = - unusedDataApply: ud => - if !tree.symbol.is(Private) then - tree.termParamss.flatten.foreach { p => - ud.addIgnoredParam(p.symbol) - } - ud.registerTrivial(tree) - traverseAnnotations(tree.symbol) - ud.registerDef(tree) - ud.addIgnoredUsage(tree.symbol) - - override def prepareForTypeDef(tree: tpd.TypeDef)(using Context): Context = - unusedDataApply: ud => - traverseAnnotations(tree.symbol) - if !tree.symbol.is(Param) then // Ignore type parameter (as Scala 2) - ud.registerDef(tree) - ud.addIgnoredUsage(tree.symbol) - - override def prepareForBind(tree: tpd.Bind)(using Context): Context = - traverseAnnotations(tree.symbol) - unusedDataApply(_.registerPatVar(tree)) + resolveUsage(tree.tpe.classSymbol, tree.name, tree.tpe.importPrefix.skipPackageObject) + refInfos.isAssignment = false + tree - override def prepareForTypeTree(tree: tpd.TypeTree)(using Context): Context = - if !tree.isInstanceOf[tpd.InferredTypeTree] then typeTraverser(unusedDataApply).traverse(tree.tpe) - ctx + // import x.y; y may be rewritten x.y, also import x.z as y + override def transformSelect(tree: Select)(using Context): tree.type = + refInfos.isAssignment = tree.hasAttachment(AssignmentTarget) + val name = tree.removeAttachment(OriginalName).getOrElse(nme.NO_NAME) + inline def isImportable = tree.qualifier.srcPos.isSynthetic + && tree.qualifier.tpe.match + case ThisType(_) | SuperType(_, _) => false + case qualtpe => qualtpe.isStable + if tree.srcPos.isSynthetic && tree.symbol == defn.TypeTest_unapply then + tree.qualifier.tpe.underlying.finalResultType match + case AppliedType(tycon, args) => + val res = + if tycon.typeSymbol == defn.TypeTestClass then args(1) // T in TypeTest[-S, T] + else if tycon.typeSymbol == defn.TypeableType then args(0) // T in Typeable[T] + else return tree + val target = res.dealias.typeSymbol + resolveUsage(target, target.name, res.importPrefix.skipPackageObject) // case _: T => + case _ => + else if isImportable || name.exists(_ != tree.symbol.name) then + if !ignoreTree(tree) then + resolveUsage(tree.symbol, name, tree.qualifier.tpe) + else if !ignoreTree(tree) then + refUsage(tree.symbol) + refInfos.isAssignment = false + tree - override def prepareForAssign(tree: tpd.Assign)(using Context): Context = - unusedDataApply{ ud => - val sym = tree.lhs.symbol - if sym.exists then - ud.registerSetVar(sym) - } + override def transformLiteral(tree: Literal)(using Context): tree.type = + tree.getAttachment(Typer.AdaptedTree).foreach(transformAllDeep) + tree - // ========== MiniPhase Transform ========== + override def prepareForCaseDef(tree: CaseDef)(using Context): Context = + nowarner.traverse(tree.pat) + ctx - override def transformBlock(tree: tpd.Block)(using Context): tpd.Tree = - popOutBlockTemplatePackageDef() + override def prepareForApply(tree: Apply)(using Context): Context = + // ignore tupling of for assignments, as they are not usages of vars + if tree.hasAttachment(ForArtifact) then + tree match + case Apply(TypeApply(Select(fun, nme.apply), _), args) => + if fun.symbol.is(Module) && defn.isTupleClass(fun.symbol.companionClass) then + args.foreach(_.withAttachment(ForArtifact, ())) + case _ => + ctx + + override def prepareForAssign(tree: Assign)(using Context): Context = + tree.lhs.putAttachment(AssignmentTarget, ()) // don't take LHS reference as a read + ctx + override def transformAssign(tree: Assign)(using Context): tree.type = + tree.lhs.removeAttachment(AssignmentTarget) tree - override def transformTemplate(tree: tpd.Template)(using Context): tpd.Tree = - popOutBlockTemplatePackageDef() + override def prepareForMatch(tree: Match)(using Context): Context = + // allow case.pat against tree.selector (simple var pat only for now) + tree.selector match + case Ident(nm) => tree.cases.foreach(k => allowVariableBindings(List(nm), List(k.pat))) + case _ => + ctx + override def transformMatch(tree: Match)(using Context): tree.type = + if tree.isInstanceOf[InlineMatch] && tree.selector.isEmpty then + val sf = defn.Compiletime_summonFrom + resolveUsage(sf, sf.name, NoPrefix) tree - override def transformPackageDef(tree: tpd.PackageDef)(using Context): tpd.Tree = - popOutBlockTemplatePackageDef() + override def transformTypeTree(tree: TypeTree)(using Context): tree.type = + tree.tpe match + case AnnotatedType(_, annot) => transformAllDeep(annot.tree) + case tpt if !tree.isInferred && tpt.typeSymbol.exists => resolveUsage(tpt.typeSymbol, tpt.typeSymbol.name, NoPrefix) + case _ => tree - override def transformValDef(tree: tpd.ValDef)(using Context): tpd.Tree = - unusedDataApply(_.removeIgnoredUsage(tree.symbol)) + override def prepareForInlined(tree: Inlined)(using Context): Context = + refInfos.inlined.push(tree.call.srcPos) + ctx + override def transformInlined(tree: Inlined)(using Context): tree.type = + //transformAllDeep(tree.expansion) // traverse expansion with nonempty inlined stack to avoid registering defs + val _ = refInfos.inlined.pop() + if !tree.call.isEmpty && phaseMode.eq(PhaseMode.Aggregate) then + transformAllDeep(tree.call) tree - override def transformDefDef(tree: tpd.DefDef)(using Context): tpd.Tree = - unusedDataApply(_.removeIgnoredUsage(tree.symbol)) + override def prepareForBind(tree: Bind)(using Context): Context = + refInfos.register(tree) + ctx + + override def prepareForValDef(tree: ValDef)(using Context): Context = + if !tree.symbol.is(Deferred) && tree.rhs.symbol != defn.Predef_undefined then + refInfos.register(tree) + relax(tree.rhs, tree.tpt.tpe) + ctx + override def transformValDef(tree: ValDef)(using Context): tree.type = + traverseAnnotations(tree.symbol) + if tree.name.startsWith("derived$") && tree.hasType then + def loop(t: Tree): Unit = t match + case Ident(name) => resolveUsage(t.tpe.typeSymbol, name, t.tpe.underlyingPrefix.skipPackageObject) + case Select(t, _) => loop(t) + case _ => + tree.getAttachment(OriginalTypeClass).foreach(loop) tree - override def transformTypeDef(tree: tpd.TypeDef)(using Context): tpd.Tree = - unusedDataApply(_.removeIgnoredUsage(tree.symbol)) + override def prepareForDefDef(tree: DefDef)(using Context): Context = + def trivial = tree.symbol.is(Deferred) || isUnconsuming(tree.rhs) + def nontrivial = tree.symbol.isConstructor || tree.symbol.isAnonymousFunction + def isDefault = tree.symbol.name.is(DefaultGetterName) + if !nontrivial && trivial || isDefault then + refInfos.skip.addOne(tree.symbol) + if tree.symbol.is(Inline) then + refInfos.inliners += 1 + else if !tree.symbol.is(Deferred) && tree.rhs.symbol != defn.Predef_undefined then + refInfos.register(tree) + relax(tree.rhs, tree.tpt.tpe) + ctx + override def transformDefDef(tree: DefDef)(using Context): tree.type = + traverseAnnotations(tree.symbol) + if tree.symbol.is(Inline) then + refInfos.inliners -= 1 tree + override def transformTypeDef(tree: TypeDef)(using Context): tree.type = + traverseAnnotations(tree.symbol) + if !tree.symbol.is(Param) then // type parameter to do? + refInfos.register(tree) + tree - // ---------- MiniPhase HELPERS ----------- + override def prepareForTemplate(tree: Template)(using Context): Context = + ctx.fresh.setProperty(resolvedKey, Resolved()) + + override def prepareForPackageDef(tree: PackageDef)(using Context): Context = + ctx.fresh.setProperty(resolvedKey, Resolved()) + + override def prepareForStats(trees: List[Tree])(using Context): Context = + ctx.fresh.setProperty(resolvedKey, Resolved()) + + override def transformOther(tree: Tree)(using Context): tree.type = + tree match + case imp: Import => + if phaseMode eq PhaseMode.Aggregate then + refInfos.register(imp) + transformAllDeep(imp.expr) + for selector <- imp.selectors do + if selector.isGiven then + selector.bound match + case untpd.TypedSplice(bound) => transformAllDeep(bound) + case _ => + case exp: Export => + transformAllDeep(exp.expr) + case AppliedTypeTree(tpt, args) => + transformAllDeep(tpt) + args.foreach(transformAllDeep) + case RefinedTypeTree(tpt, refinements) => + transformAllDeep(tpt) + refinements.foreach(transformAllDeep) + case LambdaTypeTree(tparams, body) => + tparams.foreach(transformAllDeep) + transformAllDeep(body) + case SingletonTypeTree(ref) => + // selftype of object is not a usage + val moduleSelfRef = ctx.owner.is(Module) && ctx.owner == tree.symbol.companionModule.moduleClass + if !moduleSelfRef then + transformAllDeep(ref) + case TypeBoundsTree(lo, hi, alias) => + transformAllDeep(lo) + transformAllDeep(hi) + transformAllDeep(alias) + case tree: NamedArg => transformAllDeep(tree.arg) + case Annotated(arg, annot) => + transformAllDeep(arg) + transformAllDeep(annot) + case Quote(body, tags) => + transformAllDeep(body) + tags.foreach(transformAllDeep) + case Splice(expr) => + transformAllDeep(expr) + case pat @ SplicePattern(body, args) => + transformAllDeep(body) + args.foreach(transformAllDeep) + case MatchTypeTree(bound, selector, cases) => + transformAllDeep(bound) + transformAllDeep(selector) + cases.foreach(transformAllDeep) + case ByNameTypeTree(result) => + transformAllDeep(result) + //case _: InferredTypeTree => // do nothing + //case _: Export => // nothing to do + //case _ if tree.isType => + case _ => + tree - private def pushInBlockTemplatePackageDef(tree: tpd.Block | tpd.Template | tpd.PackageDef)(using Context): Context = - unusedDataApply { ud => - ud.pushScope(UnusedData.ScopeType.fromTree(tree)) - } - ctx + private def traverseAnnotations(sym: Symbol)(using Context): Unit = + for annot <- sym.denot.annotations do + transformAllDeep(annot.tree) - private def popOutBlockTemplatePackageDef()(using Context): Context = - unusedDataApply { ud => - ud.popScope() - } - ctx + // if sym is not an enclosing element, record the reference + def refUsage(sym: Symbol)(using Context): Unit = + if !ctx.outersIterator.exists(cur => cur.owner eq sym) then + refInfos.addRef(sym) - /** - * This traverse is the **main** component of this phase + /** Look up a reference in enclosing contexts to determine whether it was introduced by a definition or import. + * The binding of highest precedence must then be correct. * - * It traverses the tree and gathers the data in the - * corresponding context property + * Unqualified locals and fully qualified globals are neither imported nor in scope; + * e.g., in `scala.Int`, `scala` is in scope for typer, but here we reverse-engineer the attribution. + * For Select, lint does not look up `.scala` (so top-level syms look like magic) but records `scala.Int`. + * For Ident, look-up finds the root import as usual. A competing import is OK because higher precedence. */ - private def traverser = new TreeTraverser: - import tpd.* - import UnusedData.ScopeType - - /* Register every imports, definition and usage */ - override def traverse(tree: tpd.Tree)(using Context): Unit = - val newCtx = if tree.symbol.exists then ctx.withOwner(tree.symbol) else ctx - tree match - case imp: tpd.Import => - unusedDataApply(_.registerImport(imp)) - imp.selectors.filter(_.isGiven).map(_.bound).collect { - case untpd.TypedSplice(tree1) => tree1 - }.foreach(traverse(_)(using newCtx)) - traverseChildren(tree)(using newCtx) - case ident: Ident => - prepareForIdent(ident) - traverseChildren(tree)(using newCtx) - case sel: Select => - prepareForSelect(sel) - traverseChildren(tree)(using newCtx) - case tree: (tpd.Block | tpd.Template | tpd.PackageDef) => - //! DIFFERS FROM MINIPHASE - pushInBlockTemplatePackageDef(tree) - traverseChildren(tree)(using newCtx) - popOutBlockTemplatePackageDef() - case t: tpd.ValDef => - prepareForValDef(t) - traverseChildren(tree)(using newCtx) - transformValDef(t) - case t: tpd.DefDef => - prepareForDefDef(t) - traverseChildren(tree)(using newCtx) - transformDefDef(t) - case t: tpd.TypeDef => - prepareForTypeDef(t) - traverseChildren(tree)(using newCtx) - transformTypeDef(t) - case t: tpd.Bind => - prepareForBind(t) - traverseChildren(tree)(using newCtx) - case t:tpd.Assign => - prepareForAssign(t) - traverseChildren(tree) - case _: tpd.InferredTypeTree => - case t@tpd.RefinedTypeTree(tpt, refinements) => - //! DIFFERS FROM MINIPHASE - typeTraverser(unusedDataApply).traverse(t.tpe) - traverse(tpt)(using newCtx) - case t@tpd.TypeTree() => - //! DIFFERS FROM MINIPHASE - typeTraverser(unusedDataApply).traverse(t.tpe) - traverseChildren(tree)(using newCtx) - case _ => - //! DIFFERS FROM MINIPHASE - traverseChildren(tree)(using newCtx) - end traverse - end traverser - - /** This is a type traverser which catch some special Types not traversed by the term traverser above */ - private def typeTraverser(dt: (UnusedData => Any) => Unit)(using Context) = new TypeTraverser: - override def traverse(tp: Type): Unit = - if tp.typeSymbol.exists then dt(_.registerUsed(tp.typeSymbol, Some(tp.typeSymbol.name))) - tp match - case AnnotatedType(_, annot) => - dt(_.registerUsed(annot.symbol, None)) - traverseChildren(tp) - case _ => - traverseChildren(tp) - - /** This traverse the annotations of the symbol */ - private def traverseAnnotations(sym: Symbol)(using Context): Unit = - sym.denot.annotations.foreach(annot => traverser.traverse(annot.tree)) - - - /** Do the actual reporting given the result of the anaylsis */ - private def reportUnused(res: UnusedData.UnusedResult)(using Context): Unit = - res.warnings.toList.sortBy(_.pos.span.point)(using Ordering[Int]).foreach { s => - s match - case UnusedSymbol(t, _, WarnTypes.Imports) => - report.warning(UnusedSymbolMessage.imports, t) - case UnusedSymbol(t, _, WarnTypes.LocalDefs) => - report.warning(UnusedSymbolMessage.localDefs, t) - case UnusedSymbol(t, _, WarnTypes.ExplicitParams) => - report.warning(UnusedSymbolMessage.explicitParams, t) - case UnusedSymbol(t, _, WarnTypes.ImplicitParams) => - report.warning(UnusedSymbolMessage.implicitParams, t) - case UnusedSymbol(t, _, WarnTypes.PrivateMembers) => - report.warning(UnusedSymbolMessage.privateMembers, t) - case UnusedSymbol(t, _, WarnTypes.PatVars) => - report.warning(UnusedSymbolMessage.patVars, t) - case UnusedSymbol(t, _, WarnTypes.UnsetLocals) => - report.warning("unset local variable, consider using an immutable val instead", t) - case UnusedSymbol(t, _, WarnTypes.UnsetPrivates) => - report.warning("unset private variable, consider using an immutable val instead", t) - } - + def resolveUsage(sym0: Symbol, name: Name, prefix: Type)(using Context): Unit = + import PrecedenceLevels.* + val sym = sym0.userSymbol + + def matchingSelector(info: ImportInfo): ImportSelector | Null = + val qtpe = info.site + def hasAltMember(nm: Name) = qtpe.member(nm).hasAltWith: alt => + val sameSym = + alt.symbol == sym + || nm.isTypeName && alt.symbol.isAliasType && alt.info.dealias.typeSymbol == sym + sameSym && alt.symbol.isAccessibleFrom(qtpe) + def loop(sels: List[ImportSelector]): ImportSelector | Null = sels match + case sel :: sels => + val matches = + if sel.isWildcard then + // if name is different from sym.name, it must be a rename on import, not a wildcard selector + !name.exists(_.toTermName != sym.name.toTermName) + // the qualifier must have the target symbol as a member + && hasAltMember(sym.name) + && { + if sel.isGiven then // Further check that the symbol is a given or implicit and conforms to the bound + sym.isOneOf(GivenOrImplicit) + && (sel.bound.isEmpty || sym.info.finalResultType <:< sel.boundTpe) + && (prefix.eq(NoPrefix) || qtpe =:= prefix) + else + !sym.is(Given) // Normal wildcard, check that the symbol is not a given (but can be implicit) + } + else + // if there is an explicit name, it must match + !name.exists(_.toTermName != sel.rename) + && (prefix.eq(NoPrefix) || qtpe =:= prefix) + && (hasAltMember(sel.name) || hasAltMember(sel.name.toTypeName)) + if matches then sel else loop(sels) + case nil => null + loop(info.selectors) + + def checkMember(ctxsym: Symbol): Boolean = + ctxsym.isClass && sym.owner.isClass + && ctxsym.thisType.baseClasses.contains(sym.owner) + && ctxsym.thisType.member(sym.name).hasAltWith(d => d.containsSym(sym) && !name.exists(_ != d.name)) + + // Attempt to cache a result at the given context. Not all contexts bear a cache, including NoContext. + // If there is already any result for the name and prefix, do nothing. + def addCached(where: Context, result: Precedence): Unit = + if where.moreProperties ne null then + where.property(resolvedKey) match + case Some(resolved) => + resolved.record(sym, name, prefix, result) + case none => + + // Avoid spurious NoSymbol and also primary ctors which are never warned about. + // Selections C.this.toString should be already excluded, but backstopped here for eq, etc. + if !sym.exists || sym.isPrimaryConstructor || sym.isEffectiveRoot || defn.topClasses(sym.owner) then return + + // Find the innermost, highest precedence. Contexts have no nesting levels but assume correctness. + // If the sym is an enclosing definition (the owner of a context), it does not count toward usages. + val isLocal = sym.isLocalToBlock + var candidate: Context = NoContext + var cachePoint: Context = NoContext // last context with Resolved cache + var importer: ImportSelector | Null = null // non-null for import context + var precedence = NoPrecedence // of current resolution + var enclosed = false // true if sym is owner of an enclosing context + var done = false + var cached = false + val ctxs = ctx.outersIterator + while !done && ctxs.hasNext do + val cur = ctxs.next() + if cur.owner.userSymbol == sym && !sym.is(Package) then + enclosed = true // found enclosing definition, don't register the reference + if isLocal then + if cur.owner eq sym.owner then + done = true // for local def, just checking that it is not enclosing + else + val cachedPrecedence = + cur.property(resolvedKey) match + case Some(resolved) => + // conservative, cache must be nested below the result context + if precedence.isNone then + cachePoint = cur // no result yet, and future result could be cached here + resolved.hasRecord(sym, name, prefix) + case none => NoPrecedence + cached = !cachedPrecedence.isNone + if cached then + // if prefer cached precedence, then discard previous result + if precedence.weakerThan(cachedPrecedence) then + candidate = NoContext + importer = null + cachePoint = cur // actual cache context + precedence = cachedPrecedence // actual cached precedence + done = true + else if cur.isImportContext then + val sel = matchingSelector(cur.importInfo.nn) + if sel != null then + if cur.importInfo.nn.isRootImport then + if precedence.weakerThan(OtherUnit) then + precedence = OtherUnit + candidate = cur + importer = sel + done = true + else if sel.isWildcard then + if precedence.weakerThan(Wildcard) then + precedence = Wildcard + candidate = cur + importer = sel + else + if precedence.weakerThan(NamedImport) then + precedence = NamedImport + candidate = cur + importer = sel + else if checkMember(cur.owner) then + if sym.is(Package) || sym.srcPos.sourcePos.source == ctx.source then + precedence = Definition + candidate = cur + importer = null // ignore import in same scope; we can't check nesting level + done = true + else if precedence.weakerThan(OtherUnit) then + precedence = OtherUnit + candidate = cur + end while + // record usage and possibly an import + if !enclosed then + refInfos.addRef(sym) + if candidate != NoContext && candidate.isImportContext && importer != null then + refInfos.sels.put(importer, ()) + // possibly record that we have performed this look-up + // if no result was found, take it as Definition (local or rooted head of fully qualified path) + val adjusted = if precedence.isNone then Definition else precedence + if !cached && (cachePoint ne NoContext) then + addCached(cachePoint, adjusted) + if cachePoint ne ctx then + addCached(ctx, adjusted) // at this ctx, since cachePoint may be far up the outer chain + end resolveUsage end CheckUnused object CheckUnused: - val phaseNamePrefix: String = "checkUnused" - val description: String = "check for unused elements" enum PhaseMode: case Aggregate case Report - private enum WarnTypes: - case Imports - case LocalDefs - case ExplicitParams - case ImplicitParams - case PrivateMembers - case PatVars - case UnsetLocals - case UnsetPrivates - - /** - * The key used to retrieve the "unused entity" analysis metadata, - * from the compilation `Context` - */ - private val _key = Property.StickyKey[UnusedData] + val refInfosKey = Property.StickyKey[RefInfos] - val OriginalName = Property.StickyKey[Name] + val resolvedKey = Property.Key[Resolved] - class PostTyper extends CheckUnused(PhaseMode.Aggregate, "PostTyper", _key) + inline def refInfos(using Context): RefInfos = ctx.property(refInfosKey).get - class PostInlining extends CheckUnused(PhaseMode.Report, "PostInlining", _key) + inline def resolved(using Context): Resolved = + ctx.property(resolvedKey) match + case Some(res) => res + case _ => throw new MatchError("no Resolved for context") - /** - * A stateful class gathering the infos on : - * - imports - * - definitions - * - usage - */ - private class UnusedData: - import collection.mutable.{Set => MutSet, Map => MutMap, Stack => MutStack, ListBuffer => MutList} - import UnusedData.* - - /** The current scope during the tree traversal */ - val currScopeType: MutStack[ScopeType] = MutStack(ScopeType.Other) - - var unusedAggregate: Option[UnusedResult] = None - - /* IMPORTS */ - private val impInScope = MutStack(MutList[ImportSelectorData]()) - /** - * We store the symbol along with their accessibility without import. - * Accessibility to their definition in outer context/scope - * - * See the `isAccessibleAsIdent` extension method below in the file - */ - private val usedInScope = MutStack(MutSet[(Symbol, Option[Name], Boolean)]()) - private val usedInPosition = MutMap.empty[Name, MutSet[Symbol]] - /* unused import collected during traversal */ - private val unusedImport = MutList.empty[ImportSelectorData] - - /* LOCAL DEF OR VAL / Private Def or Val / Pattern variables */ - private val localDefInScope = MutList.empty[tpd.MemberDef] - private val privateDefInScope = MutList.empty[tpd.MemberDef] - private val explicitParamInScope = MutList.empty[tpd.MemberDef] - private val implicitParamInScope = MutList.empty[tpd.MemberDef] - private val patVarsInScope = MutList.empty[tpd.Bind] - - /** All variables sets*/ - private val setVars = MutSet[Symbol]() - - /** All used symbols */ - private val usedDef = MutSet[Symbol]() - /** Do not register as used */ - private val doNotRegister = MutSet[Symbol]() - - /** Trivial definitions, avoid registering params */ - private val trivialDefs = MutSet[Symbol]() - - private val paramsToSkip = MutSet[Symbol]() - - - def finishAggregation(using Context)(): Unit = - val unusedInThisStage = this.getUnused - this.unusedAggregate match { - case None => - this.unusedAggregate = Some(unusedInThisStage) - case Some(prevUnused) => - val intersection = unusedInThisStage.warnings.intersect(prevUnused.warnings) - this.unusedAggregate = Some(UnusedResult(intersection)) - } + /** Attachment holding the name of an Ident as written by the user. */ + val OriginalName = Property.StickyKey[Name] + /** Suppress warning in a tree, such as a patvar name allowed by special convention. */ + val NoWarn = Property.StickyKey[Unit] - /** - * Register a found (used) symbol along with its name - * - * The optional name will be used to target the right import - * as the same element can be imported with different renaming - */ - def registerUsed(sym: Symbol, name: Option[Name], includeForImport: Boolean = true, isDerived: Boolean = false)(using Context): Unit = - if sym.exists && !isConstructorOfSynth(sym) && !doNotRegister(sym) then - if sym.isConstructor then - registerUsed(sym.owner, None, includeForImport) // constructor are "implicitly" imported with the class - else - // If the symbol is accessible in this scope without an import, do not register it for unused import analysis - val includeForImport1 = - includeForImport - && (name.exists(_.toTermName != sym.name.toTermName) || !sym.isAccessibleAsIdent) - - def addIfExists(sym: Symbol): Unit = - if sym.exists then - usedDef += sym - if includeForImport1 then - usedInScope.top += ((sym, name, isDerived)) - addIfExists(sym) - addIfExists(sym.companionModule) - addIfExists(sym.companionClass) - if sym.sourcePos.exists then - for n <- name do - usedInPosition.getOrElseUpdate(n, MutSet.empty) += sym - - /** Register a symbol that should be ignored */ - def addIgnoredUsage(sym: Symbol)(using Context): Unit = - doNotRegister ++= sym.everySymbol - - /** Remove a symbol that shouldn't be ignored anymore */ - def removeIgnoredUsage(sym: Symbol)(using Context): Unit = - doNotRegister --= sym.everySymbol - - def addIgnoredParam(sym: Symbol)(using Context): Unit = - paramsToSkip += sym - - /** Register an import */ - def registerImport(imp: tpd.Import)(using Context): Unit = - if - !tpd.languageImport(imp.expr).nonEmpty - && !imp.isGeneratedByEnum - && !isTransparentAndInline(imp) - && currScopeType.top != ScopeType.ReplWrapper // #18383 Do not report top-level import's in the repl as unused - then - val qualTpe = imp.expr.tpe - - // Put wildcard imports at the end, because they have lower priority within one Import - val reorderdSelectors = - val (wildcardSels, nonWildcardSels) = imp.selectors.partition(_.isWildcard) - nonWildcardSels ::: wildcardSels - - val excludedMembers: mutable.Set[TermName] = mutable.Set.empty - - val newDataInScope = - for sel <- reorderdSelectors yield - val data = new ImportSelectorData(qualTpe, sel) - if shouldSelectorBeReported(imp, sel) || isImportExclusion(sel) || isImportIgnored(imp, sel) then - // Immediately mark the selector as used - data.markUsed() - if isImportExclusion(sel) then - excludedMembers += sel.name - if sel.isWildcard && excludedMembers.nonEmpty then - // mark excluded members for the wildcard import - data.markExcluded(excludedMembers.toSet) - data - impInScope.top.appendAll(newDataInScope) - end registerImport - - /** Register (or not) some `val` or `def` according to the context, scope and flags */ - def registerDef(memDef: tpd.MemberDef)(using Context): Unit = - if memDef.isValidMemberDef && !isDefIgnored(memDef) then - if memDef.isValidParam then - if memDef.symbol.isOneOf(GivenOrImplicit) then - if !paramsToSkip.contains(memDef.symbol) then - implicitParamInScope += memDef - else if !paramsToSkip.contains(memDef.symbol) then - explicitParamInScope += memDef - else if currScopeType.top == ScopeType.Local then - localDefInScope += memDef - else if memDef.shouldReportPrivateDef then - privateDefInScope += memDef - - /** Register pattern variable */ - def registerPatVar(patvar: tpd.Bind)(using Context): Unit = - if !patvar.symbol.isUnusedAnnot then - patVarsInScope += patvar - - /** enter a new scope */ - def pushScope(newScopeType: ScopeType): Unit = - // unused imports : - currScopeType.push(newScopeType) - impInScope.push(MutList()) - usedInScope.push(MutSet()) - - def registerSetVar(sym: Symbol): Unit = - setVars += sym - - /** - * leave the current scope and do : - * - * - If there are imports in this scope check for unused ones - */ - def popScope()(using Context): Unit = - currScopeType.pop() - val usedInfos = usedInScope.pop() - val selDatas = impInScope.pop() - - for usedInfo <- usedInfos do - val (sym, optName, isDerived) = usedInfo - val usedData = selDatas.find { selData => - sym.isInImport(selData, optName, isDerived) - } - usedData match - case Some(data) => - data.markUsed() - case None => - // Propagate the symbol one level up - if usedInScope.nonEmpty then - usedInScope.top += usedInfo - end for // each in `used` - - for selData <- selDatas do - if !selData.isUsed then - unusedImport += selData - end popScope - - /** - * Leave the scope and return a `List` of unused `ImportSelector`s - * - * The given `List` is sorted by line and then column of the position - */ + /** Ignore reference. */ + val Ignore = Property.StickyKey[Unit] - def getUnused(using Context): UnusedResult = - popScope() + /** Tree is LHS of Assign. */ + val AssignmentTarget = Property.StickyKey[Unit] - def isUsedInPosition(name: Name, span: Span): Boolean = - usedInPosition.get(name) match - case Some(syms) => syms.exists(sym => span.contains(sym.span)) - case None => false + class PostTyper extends CheckUnused(PhaseMode.Aggregate, "PostTyper") - val sortedImp = - if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then - unusedImport.toList - .map(d => UnusedSymbol(d.selector.srcPos, d.selector.name, WarnTypes.Imports)) - else - Nil - // Partition to extract unset local variables from usedLocalDefs - val (usedLocalDefs, unusedLocalDefs) = - if ctx.settings.WunusedHas.locals then - localDefInScope.toList.partition(d => d.symbol.usedDefContains) - else - (Nil, Nil) - val sortedLocalDefs = - unusedLocalDefs - .filterNot(d => isUsedInPosition(d.symbol.name, d.span)) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.LocalDefs)) - val unsetLocalDefs = usedLocalDefs.filter(isUnsetVarDef).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetLocals)).toList - - val sortedExplicitParams = - if ctx.settings.WunusedHas.explicits then - explicitParamInScope.toList - .filterNot(d => d.symbol.usedDefContains) - .filterNot(d => isUsedInPosition(d.symbol.name, d.span)) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.ExplicitParams)) - else - Nil - val sortedImplicitParams = - if ctx.settings.WunusedHas.implicits then - implicitParamInScope.toList - .filterNot(d => d.symbol.usedDefContains) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.ImplicitParams)) - else - Nil - // Partition to extract unset private variables from usedPrivates - val (usedPrivates, unusedPrivates) = - if ctx.settings.WunusedHas.privates then - privateDefInScope.toList.partition(d => d.symbol.usedDefContains) - else - (Nil, Nil) - val sortedPrivateDefs = unusedPrivates.filterNot(d => containsSyntheticSuffix(d.symbol)).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.PrivateMembers)) - val unsetPrivateDefs = usedPrivates.filter(isUnsetVarDef).map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.UnsetPrivates)) - val sortedPatVars = - if ctx.settings.WunusedHas.patvars then - patVarsInScope.toList - .filterNot(d => d.symbol.usedDefContains) - .filterNot(d => containsSyntheticSuffix(d.symbol)) - .filterNot(d => isUsedInPosition(d.symbol.name, d.span)) - .map(d => UnusedSymbol(d.namePos, d.name, WarnTypes.PatVars)) + class PostInlining extends CheckUnused(PhaseMode.Report, "PostInlining") + + class RefInfos: + val defs = mutable.Set.empty[(Symbol, SrcPos)] // definitions + val pats = mutable.Set.empty[(Symbol, SrcPos)] // pattern variables + val refs = mutable.Set.empty[Symbol] // references + val asss = mutable.Set.empty[Symbol] // targets of assignment + val skip = mutable.Set.empty[Symbol] // methods to skip (don't warn about their params) + val nowarn = mutable.Set.empty[Symbol] // marked @nowarn + val imps = new IdentityHashMap[Import, Unit] // imports + val sels = new IdentityHashMap[ImportSelector, Unit] // matched selectors + def register(tree: Tree)(using Context): Unit = if inlined.isEmpty then + tree match + case imp: Import => + if inliners == 0 + && languageImport(imp.expr).isEmpty + && !imp.isGeneratedByEnum + && !ctx.owner.name.isReplWrapperName + then + imps.put(imp, ()) + case tree: Bind => + if !tree.name.isInstanceOf[DerivedName] && !tree.name.is(WildcardParamName) then + if tree.hasAttachment(NoWarn) then + nowarn.addOne(tree.symbol) + pats.addOne((tree.symbol, tree.namePos)) + case tree: NamedDefTree => + if tree.hasAttachment(PatternVar) then + if !tree.name.isInstanceOf[DerivedName] then + pats.addOne((tree.symbol, tree.namePos)) + else if (tree.symbol ne NoSymbol) + && !tree.name.isWildcard + && !tree.symbol.is(ModuleVal) // track only the ModuleClass using the object symbol, with correct namePos + then + if tree.hasAttachment(NoWarn) then + nowarn.addOne(tree.symbol) + defs.addOne((tree.symbol.userSymbol, tree.namePos)) + case _ => + if tree.symbol ne NoSymbol then + defs.addOne((tree.symbol, tree.srcPos)) // TODO is this a code path + + val inlined = Stack.empty[SrcPos] // enclosing call.srcPos of inlined code (expansions) + var inliners = 0 // depth of inline def (not inlined yet) + + // instead of refs.addOne, use addRef to distinguish a read from a write to var + var isAssignment = false + def addRef(sym: Symbol): Unit = + if isAssignment then + asss.addOne(sym) + else + refs.addOne(sym) + end RefInfos + + // Symbols already resolved in the given Context (with name and prefix of lookup). + class Resolved: + import PrecedenceLevels.* + private val seen = mutable.Map.empty[Symbol, List[(Name, Type, Precedence)]].withDefaultValue(Nil) + // if a result has been recorded, return it; otherwise, NoPrecedence. + def hasRecord(symbol: Symbol, name: Name, prefix: Type)(using Context): Precedence = + seen(symbol).find((n, p, _) => n == name && p =:= prefix) match + case Some((_, _, r)) => r + case none => NoPrecedence + // "record" the look-up result, if there is not already a result for the name and prefix. + def record(symbol: Symbol, name: Name, prefix: Type, result: Precedence)(using Context): Unit = + require(NoPrecedence.weakerThan(result)) + seen.updateWith(symbol): + case svs @ Some(vs) => + if vs.exists((n, p, _) => n == name && p =:= prefix) then svs + else Some((name, prefix, result) :: vs) + case none => Some((name, prefix, result) :: Nil) + + // Names are resolved by definitions and imports, which have four precedence levels: + object PrecedenceLevels: + opaque type Precedence = Int + inline def NoPrecedence: Precedence = 5 + inline def OtherUnit: Precedence = 4 // root import or def from another compilation unit via enclosing package + inline def Wildcard: Precedence = 3 // wildcard import + inline def NamedImport: Precedence = 2 // specific import + inline def Definition: Precedence = 1 // def from this compilation unit + extension (p: Precedence) + inline def weakerThan(q: Precedence): Boolean = p > q + inline def isNone: Boolean = p == NoPrecedence + + def reportUnused()(using Context): Unit = + for (msg, pos, origin) <- warnings do + if origin.isEmpty then report.warning(msg, pos) + else report.warning(msg, pos, origin) + msg.actions.headOption.foreach(Rewrites.applyAction) + + type MessageInfo = (UnusedSymbol, SrcPos, String) // string is origin or empty + + def warnings(using Context): Array[MessageInfo] = + val actionable = ctx.settings.rewrite.value.nonEmpty + val warnings = ArrayBuilder.make[MessageInfo] + def warnAt(pos: SrcPos)(msg: UnusedSymbol, origin: String = ""): Unit = warnings.addOne((msg, pos, origin)) + val infos = refInfos + //println(infos.defs.mkString("DEFS\n", "\n", "\n---")) + //println(infos.refs.mkString("REFS\n", "\n", "\n---")) + + def checkUnassigned(sym: Symbol, pos: SrcPos) = + if sym.isLocalToBlock then + if ctx.settings.WunusedHas.locals && sym.is(Mutable) && !infos.asss(sym) then + warnAt(pos)(UnusedSymbol.unsetLocals) + else if ctx.settings.WunusedHas.privates + && sym.is(Mutable) + && (sym.is(Private) || sym.isEffectivelyPrivate) + && !sym.isSetter // tracks sym.underlyingSymbol sibling getter, check setter below + && !infos.asss(sym) + && !infos.refs(sym.owner.info.member(sym.name.asTermName.setterName).symbol) + then + warnAt(pos)(UnusedSymbol.unsetPrivates) + + def checkPrivate(sym: Symbol, pos: SrcPos) = + if ctx.settings.WunusedHas.privates + && !sym.isPrimaryConstructor + && !sym.isOneOf(SelfName | Synthetic | CaseAccessor) + && !sym.name.is(BodyRetainerName) + && !sym.isSerializationSupport + && !( sym.is(Mutable) + && sym.isSetter // tracks sym.underlyingSymbol sibling getter + && (sym.owner.is(Trait) || sym.owner.isAnonymousClass) + ) + && !infos.nowarn(sym) + then + warnAt(pos)(UnusedSymbol.privateMembers) + + def checkParam(sym: Symbol, pos: SrcPos) = + val m = sym.owner + def allowed = + val dd = defn + m.isDeprecated + || m.is(Synthetic) && !m.isAnonymousFunction + || m.hasAnnotation(defn.UnusedAnnot) // param of unused method + || sym.info.isSingleton + || m.isConstructor && m.owner.thisType.baseClasses.contains(defn.AnnotationClass) + def checkExplicit(): Unit = + // A class param is unused if its param accessor is unused. + // (The class param is not assigned to a field until constructors.) + // A local param accessor warns as a param; a private accessor as a private member. + // Avoid warning for case class elements because they are aliased via unapply (i.e. may be extracted). + if m.isPrimaryConstructor then + val alias = m.owner.info.member(sym.name) + if alias.exists then + val aliasSym = alias.symbol + if aliasSym.isAllOf(PrivateParamAccessor, butNot = CaseAccessor) + && !infos.refs(alias.symbol) + && !usedByDefaultGetter(sym, m) + then + if aliasSym.is(Local) then + if ctx.settings.WunusedHas.explicits then + warnAt(pos)(UnusedSymbol.explicitParams(aliasSym)) + else + if ctx.settings.WunusedHas.privates then + warnAt(pos)(UnusedSymbol.privateMembers) + else if ctx.settings.WunusedHas.explicits + && !sym.is(Synthetic) // param to setter is unused bc there is no field yet + && !(sym.owner.is(ExtensionMethod) && + m.paramSymss.dropWhile(_.exists(_.isTypeParam)).match + case (h :: Nil) :: _ => h == sym // param is the extended receiver + case _ => false + ) + && !sym.name.isInstanceOf[DerivedName] + && !ctx.platform.isMainMethod(m) + && !usedByDefaultGetter(sym, m) + then + warnAt(pos)(UnusedSymbol.explicitParams(sym)) + end checkExplicit + // begin + if !infos.skip(m) + && !m.isEffectivelyOverride + && !allowed + then + checkExplicit() + end checkParam + + // does the param have an alias in a default arg method that is used? + def usedByDefaultGetter(param: Symbol, meth: Symbol): Boolean = + val cls = if meth.isConstructor then meth.enclosingClass.companionModule else meth.enclosingClass + val MethName = meth.name + cls.info.decls.exists: d => + d.name match + case DefaultGetterName(MethName, _) => + d.paramSymss.exists(_.exists(p => p.name == param.name && infos.refs(p))) + case _ => false + + def checkImplicit(sym: Symbol, pos: SrcPos) = + val m = sym.owner + def allowed = + val dd = defn + m.isDeprecated + || m.is(Synthetic) + || m.hasAnnotation(dd.UnusedAnnot) // param of unused method + || sym.owner.name.isContextFunction // a ubiquitous parameter + || sym.isCanEqual + || sym.info.dealias.typeSymbol.match // more ubiquity + case dd.DummyImplicitClass | dd.SubTypeClass | dd.SameTypeClass => true + case tps => + tps.isMarkerTrait // no members to use; was only if sym.name.is(ContextBoundParamName) + || // but consider NotGiven + tps.hasAnnotation(dd.LanguageFeatureMetaAnnot) + || sym.info.isSingleton // DSL friendly + || sym.info.dealias.isInstanceOf[RefinedType] // can't be expressed as a context bound + if ctx.settings.WunusedHas.implicits + && !infos.skip(m) + && !m.isEffectivelyOverride + && !allowed + then + if m.isPrimaryConstructor then + val alias = m.owner.info.member(sym.name) + if alias.exists then + val aliasSym = alias.symbol + val checking = + aliasSym.isAllOf(PrivateParamAccessor, butNot = CaseAccessor) + || aliasSym.isAllOf(Protected | ParamAccessor, butNot = CaseAccessor) && m.owner.is(Given) + if checking + && !infos.refs(alias.symbol) + && !usedByDefaultGetter(sym, m) + then + warnAt(pos)(UnusedSymbol.implicitParams(aliasSym)) + else if !usedByDefaultGetter(sym, m) then + warnAt(pos)(UnusedSymbol.implicitParams(sym)) + + def checkLocal(sym: Symbol, pos: SrcPos) = + if ctx.settings.WunusedHas.locals + && !sym.is(InlineProxy) + && !sym.isCanEqual + then + warnAt(pos)(UnusedSymbol.localDefs) + + def checkPatvars() = + // convert the one non-synthetic span so all are comparable; filter NoSpan below + def uniformPos(sym: Symbol, pos: SrcPos): SrcPos = + if pos.span.isSynthetic then pos else pos.sourcePos.withSpan(pos.span.toSynthetic) + // patvars in for comprehensions share the pos of where the name was introduced + val byPos = infos.pats.groupMap(uniformPos(_, _))((sym, pos) => sym) + for (pos, syms) <- byPos if pos.span.exists && !syms.exists(_.hasAnnotation(defn.UnusedAnnot)) do + if !syms.exists(infos.refs(_)) then + if !syms.exists(v => !v.isLocal && !v.is(Private) || infos.nowarn(v)) then + warnAt(pos)(UnusedSymbol.patVars) + else if syms.exists(_.is(Mutable)) then // check unassigned var + val sym = // recover the original + if syms.size == 1 then syms.head + else infos.pats.find((s, p) => syms.contains(s) && !p.span.isSynthetic).map(_._1).getOrElse(syms.head) + if sym.is(Mutable) && !infos.asss(sym) then + if sym.isLocalToBlock then + warnAt(pos)(UnusedSymbol.unsetLocals) + else if sym.is(Private) then + warnAt(pos)(UnusedSymbol.unsetPrivates) + + def checkImports() = + // TODO check for unused masking import + import scala.jdk.CollectionConverters.given + import Rewrites.ActionPatch + type ImpSel = (Import, ImportSelector) + def isUsable(imp: Import, sel: ImportSelector): Boolean = + sel.isImportExclusion || infos.sels.containsKey(sel) || imp.isLoose(sel) + def warnImport(warnable: ImpSel, actions: List[CodeAction] = Nil): Unit = + val (imp, sel) = warnable + val msg = UnusedSymbol.imports(actions) + // example collection.mutable.{Map as MutMap} + val origin = cpy.Import(imp)(imp.expr, List(sel)).show(using ctx.withoutColors).stripPrefix("import ") + warnAt(sel.srcPos)(msg, origin) + + if !actionable then + for imp <- infos.imps.keySet.nn.asScala; sel <- imp.selectors if !isUsable(imp, sel) do + warnImport(imp -> sel) + else + // If the rest of the line is blank, include it in the final edit position. (Delete trailing whitespace.) + // If for deletion, and the prefix of the line is also blank, then include that, too. (Del blank line.) + // If deleting a blank line and surrounded by blank lines, remove an adjoining blank line. + def editPosAt(srcPos: SrcPos, forDeletion: Boolean): SrcPos = + val start = srcPos.span.start + val end = srcPos.span.end + val content = srcPos.sourcePos.source.content() + val prev = content.lastIndexWhere(c => !isWhitespace(c), end = start - 1) + val emptyLeft = prev < 0 || isLineBreakChar(content(prev)) + val next = content.indexWhere(c => !isWhitespace(c), from = end) + val emptyRight = next < 0 || isLineBreakChar(content(next)) + val deleteLine = emptyLeft && emptyRight && forDeletion + val bump = if (deleteLine) 1 else 0 // todo improve to include offset of next line, endline + 1 + val p0 = srcPos.span + val p1 = if (next >= 0 && emptyRight) p0.withEnd(next + bump) else p0 + val p2 = + if deleteLine then + var newStart = prev + 1 + if srcPos.line > 1 then + val source = srcPos.sourcePos.source + import source.{lineToOffset, lineToOffsetOpt, offsetToLine} + val startLine = offsetToLine(start) + val endLine = offsetToLine(end) + val preceding = lineToOffset(startLine - 1) + lineToOffsetOpt(endLine + 2) match + case Some(succeeding) if lineToOffset(startLine) - preceding == 1 && succeeding - end == 2 => + newStart = preceding + case _ => + p1.withStart(newStart) + else p1 + srcPos.sourcePos.withSpan(p2) + def actionsOf(actions: (SrcPos, String)*): List[CodeAction] = + val patches = actions.map((srcPos, replacement) => ActionPatch(srcPos.sourcePos, replacement)).toList + List(CodeAction(title = "unused import", description = Some("remove import"), patches)) + def replace(editPos: SrcPos)(replacement: String): List[CodeAction] = actionsOf(editPos -> replacement) + def deletion(editPos: SrcPos): List[CodeAction] = actionsOf(editPos -> "") + def textFor(impsel: ImpSel): String = + val (imp, sel) = impsel + val content = imp.srcPos.sourcePos.source.content() + def textAt(pos: SrcPos) = String(content.slice(pos.span.start, pos.span.end)) + val qual = textAt(imp.expr.srcPos) // keep original + val selector = textAt(sel.srcPos) // keep original + s"$qual.$selector" // don't succumb to vagaries of show + // begin actionable + val sortedImps = infos.imps.keySet.nn.asScala + .filter(_.srcPos.span.exists) // extra caution + .toArray + .sortBy(_.srcPos.span.point) // sorted by pos, not sort in place + var index = 0 + while index < sortedImps.length do + val nextImport = sortedImps.indexSatisfying(from = index + 1)(_.isPrimaryClause) // next import statement + if sortedImps.indexSatisfying(from = index, until = nextImport): imp => + imp.selectors.exists(!isUsable(imp, _)) // check if any selector in statement was unused + < nextImport then + // if no usable selectors in the import statement, delete it entirely. + // if there is exactly one usable selector, then replace with just that selector (i.e., format it). + // else for each clause, delete it or format one selector or delete unused selectors. + // To delete a comma separated item, delete start-to-start, but for last item delete a preceding comma. + // Reminder that first clause span includes the keyword, so delete point-to-start instead. + val existing = sortedImps.slice(index, nextImport) + val (keeping, deleting) = existing.iterator.flatMap(imp => imp.selectors.map(imp -> _)).toList + .partition(isUsable(_, _)) + if keeping.isEmpty then + val editPos = existing.head.srcPos.sourcePos.withSpan: + Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end) + deleting.init.foreach(warnImport(_)) + warnImport(deleting.last, deletion(editPosAt(editPos, forDeletion = true))) + else if keeping.lengthIs == 1 then + val editPos = existing.head.srcPos.sourcePos.withSpan: + Span(start = existing.head.srcPos.span.start, end = existing.last.srcPos.span.end) + deleting.init.foreach(warnImport(_)) + val text = s"import ${textFor(keeping.head)}" + warnImport(deleting.last, replace(editPosAt(editPos, forDeletion = false))(text)) + else + val lostClauses = existing.iterator.filter(imp => !keeping.exists((i, _) => imp eq i)).toList + for imp <- lostClauses do + val actions = + if imp == existing.last then + val content = imp.srcPos.sourcePos.source.content() + val prev = existing.lastIndexWhere(i0 => keeping.exists((i, _) => i == i0)) + val comma = content.indexOf(',', from = existing(prev).srcPos.span.end) + val commaPos = imp.srcPos.sourcePos.withSpan: + Span(start = comma, end = existing(prev + 1).srcPos.span.start) + val srcPos = imp.srcPos + val editPos = srcPos.sourcePos.withSpan: // exclude keyword + srcPos.span.withStart(srcPos.span.point) + actionsOf(commaPos -> "", editPos -> "") + else + val impIndex = existing.indexOf(imp) + val editPos = imp.srcPos.sourcePos.withSpan: // exclude keyword + Span(start = imp.srcPos.span.point, end = existing(impIndex + 1).srcPos.span.start) + deletion(editPos) + imp.selectors.init.foreach(sel => warnImport(imp -> sel)) + warnImport(imp -> imp.selectors.last, actions) + val singletons = existing.iterator.filter(imp => keeping.count((i, _) => imp eq i) == 1).toList + var seen = List.empty[Import] + for impsel <- deleting do + val (imp, sel) = impsel + if singletons.contains(imp) then + if seen.contains(imp) then + warnImport(impsel) + else + seen ::= imp + val editPos = imp.srcPos.sourcePos.withSpan: + Span(start = imp.srcPos.span.point, end = imp.srcPos.span.end) // exclude keyword + val text = textFor(keeping.find((i, _) => imp eq i).get) + warnImport(impsel, replace(editPosAt(editPos, forDeletion = false))(text)) + else if !lostClauses.contains(imp) then + val actions = + if sel == imp.selectors.last then + val content = sel.srcPos.sourcePos.source.content() + val prev = imp.selectors.lastIndexWhere(s0 => keeping.exists((_, s) => s == s0)) + val comma = content.indexOf(',', from = imp.selectors(prev).srcPos.span.end) + val commaPos = sel.srcPos.sourcePos.withSpan: + Span(start = comma, end = imp.selectors(prev + 1).srcPos.span.start) + val editPos = sel.srcPos + actionsOf(commaPos -> "", editPos -> "") + else + val selIndex = imp.selectors.indexOf(sel) + val editPos = sel.srcPos.sourcePos.withSpan: + sel.srcPos.span.withEnd(imp.selectors(selIndex + 1).srcPos.span.start) + deletion(editPos) + warnImport(impsel, actions) + end if + index = nextImport + end while + + // begin + for (sym, pos) <- infos.defs.iterator if !sym.hasAnnotation(defn.UnusedAnnot) do + if infos.refs(sym) then + checkUnassigned(sym, pos) + else if sym.isEffectivelyPrivate then + checkPrivate(sym, pos) + else if sym.is(Param, butNot = Given | Implicit) then + checkParam(sym, pos) + else if sym.is(Param) then // Given | Implicit + checkImplicit(sym, pos) + else if sym.isLocalToBlock then + checkLocal(sym, pos) + + if ctx.settings.WunusedHas.patvars then + checkPatvars() + + if ctx.settings.WunusedHas.imports || ctx.settings.WunusedHas.strictNoImplicitWarn then + checkImports() + + def sortOrder(msgInfo: MessageInfo): Int = + val srcPos = msgInfo._2 + if srcPos.span.exists then srcPos.span.point else 0 + + warnings.result().sortBy(sortOrder) + end warnings + + // Specific exclusions + def ignoreTree(tree: Tree): Boolean = + tree.hasAttachment(ForArtifact) || tree.hasAttachment(Ignore) + + // The RHS of a def is too trivial to warn about unused params, e.g. def f(x: Int) = ??? + def isUnconsuming(rhs: Tree)(using Context): Boolean = + rhs.symbol == defn.Predef_undefined + || rhs.tpe =:= defn.NothingType // compiletime.error + || rhs.isInstanceOf[Literal] // 42 + || rhs.tpe.match + case ConstantType(_) => true + case tp: TermRef => tp.underlying.classSymbol.is(Module) // Scala 2 SingleType + case _ => false + //|| isPurePath(rhs) // a bit strong + || rhs.match + case Block((dd @ DefDef(anonfun, paramss, _, _)) :: Nil, Closure(Nil, Ident(nm), _)) => + anonfun == nm // isAnonymousFunctionName(anonfun) + && paramss.match + case (ValDef(contextual, _, _) :: Nil) :: Nil => + contextual.is(ContextFunctionParamName) + && isUnconsuming(dd.rhs) // rhs was wrapped in a context function + case _ => false + case Block(Nil, Literal(u)) => u.tpe =:= defn.UnitType // def f(x: X) = {} + case This(_) => true + case Ident(_) => rhs.symbol.is(ParamAccessor) + case Typed(rhs, _) => isUnconsuming(rhs) + case _ => false + + def allowVariableBindings(ok: List[Name], args: List[Tree]): Unit = + ok.zip(args).foreach: + case (param, arg @ Bind(p, _)) if param == p => arg.withAttachment(NoWarn, ()) + case _ => + + // NoWarn Binds if the name matches a "canonical" name, e.g. case element name + val nowarner = new TreeTraverser: + def traverse(tree: Tree)(using Context) = tree match + case UnApply(fun, _, args) => + val unapplied = tree.tpe.finalResultType.dealias.typeSymbol + if unapplied.is(CaseClass) then + allowVariableBindings(unapplied.primaryConstructor.info.firstParamNames, args) + else if fun.symbol == defn.PairClass_unapply then + val ok = fun.symbol.info match + case PolyType(tycon, MethodTpe(_, _, AppliedType(_, tprefs))) => + tprefs.collect: + case ref: TypeParamRef => termName(ref.binder.paramNames(ref.paramNum).toString.toLowerCase.nn) + case _ => Nil + allowVariableBindings(ok, args) + else if fun.symbol == defn.TypeTest_unapply then + () // just recurse into args else - Nil - val warnings = - sortedImp ::: - sortedLocalDefs ::: - sortedExplicitParams ::: - sortedImplicitParams ::: - sortedPrivateDefs ::: - sortedPatVars ::: - unsetLocalDefs ::: - unsetPrivateDefs - UnusedResult(warnings.toSet) - end getUnused - //============================ HELPERS ==================================== - - - /** - * Checks if import selects a def that is transparent and inline - */ - private def isTransparentAndInline(imp: tpd.Import)(using Context): Boolean = - imp.selectors.exists { sel => - val qual = imp.expr - val importedMembers = qual.tpe.member(sel.name).alternatives.map(_.symbol) - importedMembers.exists(s => s.is(Transparent) && s.is(Inline)) + if unapplied.exists && unapplied.owner == defn.Quotes_reflectModule then + // cheapy search for parameter names via java reflection of Trees + // in lieu of drilling into requiredClass("scala.quoted.runtime.impl.QuotesImpl") + // ...member("reflect")...member(unapplied.name.toTypeName) + // with aliases into requiredModule("dotty.tools.dotc.ast.tpd") + val implName = s"dotty.tools.dotc.ast.Trees$$${unapplied.name}" + try + import scala.language.unsafeNulls + val clz = Class.forName(implName) // TODO improve to use class path or reflect + val ok = clz.getConstructors.head.getParameters.map(p => termName(p.getName)).toList.init + allowVariableBindings(ok, args) + catch case _: ClassNotFoundException => () + args.foreach(traverse) + case tree => traverseChildren(tree) + + // NoWarn members in tree that correspond to refinements; currently uses only names. + def relax(tree: Tree, tpe: Type)(using Context): Unit = + def refinements(tpe: Type, names: List[Name]): List[Name] = + tpe match + case RefinedType(parent, refinedName, refinedInfo) => refinedName :: refinements(parent, names) + case _ => names + val refinedNames = refinements(tpe, Nil) + if !refinedNames.isEmpty then + val names = refinedNames.toSet + val relaxer = new TreeTraverser: + def traverse(tree: Tree)(using Context) = + tree match + case tree: NamedDefTree if names(tree.name) => tree.withAttachment(NoWarn, ()) + case _ => + traverseChildren(tree) + relaxer.traverse(tree) + + extension (nm: Name) + inline def exists(p: Name => Boolean): Boolean = nm.ne(nme.NO_NAME) && p(nm) + inline def isWildcard: Boolean = nm == nme.WILDCARD || nm.is(WildcardParamName) + + extension (tp: Type)(using Context) + def importPrefix: Type = tp match + case tp: NamedType => tp.prefix + case tp: ClassInfo => tp.prefix + case tp: TypeProxy => tp.superType.normalizedPrefix + case _ => NoType + def underlyingPrefix: Type = tp match + case tp: NamedType => tp.prefix + case tp: ClassInfo => tp.prefix + case tp: TypeProxy => tp.underlying.underlyingPrefix + case _ => NoType + def skipPackageObject: Type = + if tp.typeSymbol.isPackageObject then tp.underlyingPrefix else tp + def underlying: Type = tp match + case tp: TypeProxy => tp.underlying + case _ => tp + + private val serializationNames: Set[TermName] = + Set("readResolve", "readObject", "readObjectNoData", "writeObject", "writeReplace").map(termName(_)) + + extension (sym: Symbol)(using Context) + def isSerializationSupport: Boolean = + sym.is(Method) && serializationNames(sym.name.toTermName) && sym.owner.isClass + && sym.owner.derivesFrom(defn.JavaSerializableClass) + def isCanEqual: Boolean = + sym.isOneOf(GivenOrImplicit) && sym.info.finalResultType.baseClasses.exists(_.derivesFrom(defn.CanEqualClass)) + def isMarkerTrait: Boolean = + sym.info.hiBound.resultType.allMembers.forall: d => + val m = d.symbol + !m.isTerm || m.isSelfSym || m.is(Method) && (m.owner == defn.AnyClass || m.owner == defn.ObjectClass) + def isEffectivelyPrivate: Boolean = + sym.is(Private, butNot = ParamAccessor) + || sym.owner.isAnonymousClass && !sym.isEffectivelyOverride + def isEffectivelyOverride: Boolean = + sym.is(Override) + || + sym.canMatchInheritedSymbols && { // inline allOverriddenSymbols using owner.info or thisType + val owner = sym.owner.asClass + val base = if owner.classInfo.selfInfo != NoType then owner.thisType else owner.info + base.baseClasses.drop(1).iterator.exists(sym.overriddenSymbol(_).exists) } - - /** - * Heuristic to detect synthetic suffixes in names of symbols - */ - private def containsSyntheticSuffix(symbol: Symbol)(using Context): Boolean = - symbol.name.mangledString.contains("$") - - /** - * Is the constructor of synthetic package object - * Should be ignored as it is always imported/used in package - * Trigger false negative on used import - * - * Without this check example: - * - * --- WITH PACKAGE : WRONG --- - * {{{ - * package a: - * val x: Int = 0 - * package b: - * import a.* // no warning - * }}} - * --- WITH OBJECT : OK --- - * {{{ - * object a: - * val x: Int = 0 - * object b: - * import a.* // unused warning - * }}} + // pick the symbol the user wrote for purposes of tracking + inline def userSymbol: Symbol= + if sym.denot.is(ModuleClass) then sym.denot.companionModule else sym + + extension (sel: ImportSelector) + def boundTpe: Type = sel.bound match + case untpd.TypedSplice(tree) => tree.tpe + case _ => NoType + /** This is used to ignore exclusion imports of the form import `qual.member as _` + * because `sel.isUnimport` is too broad for old style `import concurrent._`. */ - private def isConstructorOfSynth(sym: Symbol)(using Context): Boolean = - sym.exists && sym.isConstructor && sym.owner.isPackageObject && sym.owner.is(Synthetic) - - /** - * This is used to avoid reporting the parameters of the synthetic main method - * generated by `@main` - */ - private def isSyntheticMainParam(sym: Symbol)(using Context): Boolean = - sym.exists && ctx.platform.isMainMethod(sym.owner) && sym.owner.is(Synthetic) - - /** - * This is used to ignore exclusion imports (i.e. import `qual`.{`member` => _}) - */ - private def isImportExclusion(sel: ImportSelector): Boolean = sel.renamed match - case untpd.Ident(name) => name == StdNames.nme.WILDCARD + def isImportExclusion: Boolean = sel.renamed match + case untpd.Ident(nme.WILDCARD) => true case _ => false - /** - * If -Wunused:strict-no-implicit-warn import and this import selector could potentially import implicit. - * return true - */ - private def shouldSelectorBeReported(imp: tpd.Import, sel: ImportSelector)(using Context): Boolean = - ctx.settings.WunusedHas.strictNoImplicitWarn && ( - sel.isWildcard || - imp.expr.tpe.member(sel.name.toTermName).alternatives.exists(_.symbol.isOneOf(GivenOrImplicit)) || - imp.expr.tpe.member(sel.name.toTypeName).alternatives.exists(_.symbol.isOneOf(GivenOrImplicit)) - ) - - /** - * Ignore CanEqual imports - */ - private def isImportIgnored(imp: tpd.Import, sel: ImportSelector)(using Context): Boolean = - (sel.isWildcard && sel.isGiven && imp.expr.tpe.allMembers.exists(p => p.symbol.typeRef.baseClasses.exists(_.derivesFrom(defn.CanEqualClass)) && p.symbol.isOneOf(GivenOrImplicit))) || - (imp.expr.tpe.member(sel.name.toTermName).alternatives - .exists(p => p.symbol.isOneOf(GivenOrImplicit) && p.symbol.typeRef.baseClasses.exists(_.derivesFrom(defn.CanEqualClass)))) - - /** - * Ignore definitions of CanEqual given - */ - private def isDefIgnored(memDef: tpd.MemberDef)(using Context): Boolean = - memDef.symbol.isOneOf(GivenOrImplicit) && memDef.symbol.typeRef.baseClasses.exists(_.derivesFrom(defn.CanEqualClass)) - - extension (tree: ImportSelector) - def boundTpe: Type = tree.bound match { - case untpd.TypedSplice(tree1) => tree1.tpe - case _ => NoType - } + extension (imp: Import)(using Context) + /** Is it the first import clause in a statement? `a.x` in `import a.x, b.{y, z}` */ + def isPrimaryClause: Boolean = + imp.srcPos.span.pointDelta > 0 // primary clause starts at `import` keyword with point at clause proper - extension (sym: Symbol) - /** is accessible without import in current context */ - private def isAccessibleAsIdent(using Context): Boolean = - ctx.outersIterator.exists{ c => - c.owner == sym.owner - || sym.owner.isClass && c.owner.isClass - && c.owner.thisType.baseClasses.contains(sym.owner) - && c.owner.thisType.member(sym.name).alternatives.contains(sym) - } - - /** Given an import and accessibility, return selector that matches import<->symbol */ - private def isInImport(selData: ImportSelectorData, altName: Option[Name], isDerived: Boolean)(using Context): Boolean = - assert(sym.exists, s"Symbol $sym does not exist") - - val selector = selData.selector - - if !selector.isWildcard then - if altName.exists(explicitName => selector.rename != explicitName.toTermName) then - // if there is an explicit name, it must match - false - else - if isDerived then - // See i15503i.scala, grep for "package foo.test.i17156" - selData.allSymbolsDealiasedForNamed.contains(dealias(sym)) - else - selData.allSymbolsForNamed.contains(sym) - else - // Wildcard - if selData.excludedMembers.contains(altName.getOrElse(sym.name).toTermName) then - // Wildcard with exclusions that match the symbol - false - else if !selData.qualTpe.member(sym.name).hasAltWith(_.symbol == sym) then - // The qualifier does not have the target symbol as a member - false - else - if selector.isGiven then - // Further check that the symbol is a given or implicit and conforms to the bound - sym.isOneOf(Given | Implicit) - && (selector.bound.isEmpty || sym.info.finalResultType <:< selector.boundTpe) - else - // Normal wildcard, check that the symbol is not a given (but can be implicit) - !sym.is(Given) - end if - end isInImport - - /** Annotated with @unused */ - private def isUnusedAnnot(using Context): Boolean = - sym.annotations.exists(a => a.symbol == ctx.definitions.UnusedAnnot) - - private def shouldNotReportParamOwner(using Context): Boolean = - if sym.exists then - val owner = sym.owner - trivialDefs(owner) || // is a trivial def - owner.isPrimaryConstructor || - owner.annotations.exists ( // @depreacated - _.symbol == ctx.definitions.DeprecatedAnnot - ) || - owner.isAllOf(Synthetic | PrivateLocal) || - owner.is(Accessor) || - owner.isOverriden - else - false - - private def usedDefContains(using Context): Boolean = - sym.everySymbol.exists(usedDef.apply) - - private def everySymbol(using Context): List[Symbol] = - List(sym, sym.companionClass, sym.companionModule, sym.moduleClass).filter(_.exists) - - /** A function is overriden. Either has `override flags` or parent has a matching member (type and name) */ - private def isOverriden(using Context): Boolean = - sym.is(Flags.Override) || (sym.exists && sym.owner.thisType.parents.exists(p => sym.matchingMember(p).exists)) - - end extension - - extension (defdef: tpd.DefDef) - // so trivial that it never consumes params - private def isTrivial(using Context): Boolean = - val rhs = defdef.rhs - rhs.symbol == ctx.definitions.Predef_undefined || - rhs.tpe =:= ctx.definitions.NothingType || - defdef.symbol.is(Deferred) || - (rhs match { - case _: tpd.Literal => true - case _ => rhs.tpe match - case ConstantType(_) => true - case tp: TermRef => - // Detect Scala 2 SingleType - tp.underlying.classSymbol.is(Flags.Module) - case _ => - false - }) - def registerTrivial(using Context): Unit = - if defdef.isTrivial then - trivialDefs += defdef.symbol - - extension (memDef: tpd.MemberDef) - private def isValidMemberDef(using Context): Boolean = - memDef.symbol.exists - && !memDef.symbol.isUnusedAnnot - && !memDef.symbol.isAllOf(Flags.AccessorCreationFlags) - && !memDef.name.isWildcard - && !memDef.symbol.owner.is(ExtensionMethod) - - private def isValidParam(using Context): Boolean = - val sym = memDef.symbol - (sym.is(Param) || sym.isAllOf(PrivateParamAccessor | Local, butNot = CaseAccessor)) && - !isSyntheticMainParam(sym) && - !sym.shouldNotReportParamOwner - - private def shouldReportPrivateDef(using Context): Boolean = - currScopeType.top == ScopeType.Template && !memDef.symbol.isConstructor && memDef.symbol.is(Private, butNot = SelfName | Synthetic | CaseAccessor) - - private def isUnsetVarDef(using Context): Boolean = - val sym = memDef.symbol - sym.is(Mutable) && !setVars(sym) - - extension (imp: tpd.Import) - /** Enum generate an import for its cases (but outside them), which should be ignored */ - def isGeneratedByEnum(using Context): Boolean = - imp.symbol.exists && imp.symbol.owner.is(Flags.Enum, butNot = Flags.Case) - - extension (thisName: Name) - private def isWildcard: Boolean = - thisName == StdNames.nme.WILDCARD || thisName.is(WildcardParamName) - - end UnusedData - - private object UnusedData: - enum ScopeType: - case Local - case Template - case ReplWrapper - case Other - - object ScopeType: - /** return the scope corresponding to the enclosing scope of the given tree */ - def fromTree(tree: tpd.Tree)(using Context): ScopeType = tree match - case tree: tpd.Template => if tree.symbol.name.isReplWrapperName then ReplWrapper else Template - case _:tpd.Block => Local - case _ => Other - - final class ImportSelectorData(val qualTpe: Type, val selector: ImportSelector): - private var myUsed: Boolean = false - var excludedMembers: Set[TermName] = Set.empty - - def markUsed(): Unit = myUsed = true - - def isUsed: Boolean = myUsed - - def markExcluded(excluded: Set[TermName]): Unit = excludedMembers ++= excluded - - private var myAllSymbols: Set[Symbol] | Null = null - - def allSymbolsForNamed(using Context): Set[Symbol] = - if myAllSymbols == null then - val allDenots = qualTpe.member(selector.name).alternatives ::: qualTpe.member(selector.name.toTypeName).alternatives - myAllSymbols = allDenots.map(_.symbol).toSet - myAllSymbols.uncheckedNN - - private var myAllSymbolsDealiased: Set[Symbol] | Null = null - - def allSymbolsDealiasedForNamed(using Context): Set[Symbol] = - if myAllSymbolsDealiased == null then - myAllSymbolsDealiased = allSymbolsForNamed.map(sym => dealias(sym)) - myAllSymbolsDealiased.uncheckedNN - end ImportSelectorData - - case class UnusedSymbol(pos: SrcPos, name: Name, warnType: WarnTypes) - /** A container for the results of the used elements analysis */ - case class UnusedResult(warnings: Set[UnusedSymbol]) - object UnusedResult: - val Empty = UnusedResult(Set.empty) - end UnusedData - - private def dealias(symbol: Symbol)(using Context): Symbol = - if symbol.isType && symbol.asType.denot.isAliasType then - symbol.asType.typeRef.dealias.typeSymbol - else - symbol + /** Generated import of cases from enum companion. */ + def isGeneratedByEnum: Boolean = + imp.symbol.exists && imp.symbol.owner.is(Enum, butNot = Case) + /** Under -Wunused:strict-no-implicit-warn, avoid false positives + * if this selector is a wildcard that might import implicits or + * specifically does import an implicit. + * Similarly, import of CanEqual must not warn, as it is always witness. + */ + def isLoose(sel: ImportSelector): Boolean = + if ctx.settings.WunusedHas.strictNoImplicitWarn then + if sel.isWildcard + || imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit)) + || imp.expr.tpe.member(sel.name.toTypeName).hasAltWith(_.symbol.isOneOf(GivenOrImplicit)) + then return true + if sel.isWildcard && sel.isGiven + then imp.expr.tpe.allMembers.exists(_.symbol.isCanEqual) + else imp.expr.tpe.member(sel.name.toTermName).hasAltWith(_.symbol.isCanEqual) + + extension (pos: SrcPos) + def isZeroExtentSynthetic: Boolean = pos.span.isSynthetic && pos.span.isZeroExtent + def isSynthetic: Boolean = pos.span.isSynthetic && pos.span.exists + + extension [A <: AnyRef](arr: Array[A]) + // returns `until` if not satisfied + def indexSatisfying(from: Int, until: Int = arr.length)(p: A => Boolean): Int = + var i = from + while i < until && !p(arr(i)) do + i += 1 + i end CheckUnused diff --git a/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala b/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala index 9433f7949163..8c3355d5f56b 100644 --- a/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala +++ b/compiler/src/dotty/tools/dotc/transform/CollectNullableFields.scala @@ -60,7 +60,7 @@ class CollectNullableFields extends MiniPhase { val sym = tree.symbol val isNullablePrivateField = sym.isField && - !sym.is(Lazy) && + !sym.isOneOf(Lazy | Erased) && !sym.owner.is(Trait) && sym.initial.isAllOf(PrivateLocal) && // We need `isNullableClassAfterErasure` and not `isNullable` because diff --git a/compiler/src/dotty/tools/dotc/transform/ContextFunctionResults.scala b/compiler/src/dotty/tools/dotc/transform/ContextFunctionResults.scala index 0bb54f8c9345..463bb68718ef 100644 --- a/compiler/src/dotty/tools/dotc/transform/ContextFunctionResults.scala +++ b/compiler/src/dotty/tools/dotc/transform/ContextFunctionResults.scala @@ -104,7 +104,7 @@ object ContextFunctionResults: def contextFunctionResultTypeAfter(meth: Symbol, depth: Int)(using Context) = def recur(tp: Type, n: Int): Type = if n == 0 then tp - else tp match + else tp.dealias match case defn.ContextFunctionType(_, resTpe) => recur(resTpe, n - 1) recur(meth.info.finalResultType, depth) diff --git a/compiler/src/dotty/tools/dotc/transform/Dependencies.scala b/compiler/src/dotty/tools/dotc/transform/Dependencies.scala index 733f4601e363..5d76158edc96 100644 --- a/compiler/src/dotty/tools/dotc/transform/Dependencies.scala +++ b/compiler/src/dotty/tools/dotc/transform/Dependencies.scala @@ -29,7 +29,7 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co def tracked: Iterable[Symbol] = free.keys /** The outermost class that captures all free variables of a function - * that are captured by enclosinh classes (this means that the function could + * that are captured by enclosing classes (this means that the function could * be placed in that class without having to add more environment parameters) */ def logicalOwner: collection.Map[Symbol, Symbol] = logicOwner @@ -136,6 +136,7 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co if !enclosure.exists then throw NoPath() if enclosure == sym.enclosure then NoSymbol else + /** is sym a constructor or a term that is nested in a constructor? */ def nestedInConstructor(sym: Symbol): Boolean = sym.isConstructor || sym.isTerm && nestedInConstructor(sym.enclosure) @@ -236,6 +237,10 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co captureImplicitThis(tree.tpe) case tree: Select => if isExpr(sym) && isLocal(sym) then markCalled(sym, enclosure) + case tree: New => + val constr = tree.tpe.typeSymbol.primaryConstructor + if constr.exists then + symSet(called, enclosure) += constr case tree: This => narrowTo(tree.symbol.asClass) case tree: MemberDef if isExpr(sym) && sym.owner.isTerm => @@ -290,7 +295,6 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co val calleeOwner = normalizedCallee.owner if calleeOwner.isTerm then narrowLogicOwner(caller, logicOwner(normalizedCallee)) else - assert(calleeOwner.is(Trait)) // methods nested inside local trait methods cannot be lifted out // beyond the trait. Note that we can also call a trait method through // a qualifier; in that case no restriction to lifted owner arises. diff --git a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala index f62044eae7d2..01875818ec74 100644 --- a/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala +++ b/compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala @@ -126,10 +126,16 @@ class ExpandSAMs extends MiniPhase: // The right hand side from which to construct the partial function. This is always a Match. // If the original rhs is already a Match (possibly in braces), return that. // Otherwise construct a match `x match case _ => rhs` where `x` is the parameter of the closure. - def partialFunRHS(tree: Tree): Match = tree match + def partialFunRHS(tree: Tree): Match = + inline def checkMatch(): Unit = + tree match + case Block(_, m: Match) => report.warning(reporting.MatchIsNotPartialFunction(), m.srcPos) + case _ => + tree match case m: Match => m case Block(Nil, expr) => partialFunRHS(expr) case _ => + checkMatch() Match(ref(param.symbol), CaseDef(untpd.Ident(nme.WILDCARD).withType(param.symbol.info), EmptyTree, tree) :: Nil) diff --git a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala index e60561fb04e2..493302961461 100644 --- a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala +++ b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala @@ -233,6 +233,7 @@ object GenericSignatures { @noinline def jsig(tp0: Type, toplevel: Boolean = false, unboxedVCs: Boolean = true): Unit = { + inline def jsig1(tp0: Type): Unit = jsig(tp0, toplevel = false, unboxedVCs = true) val tp = tp0.dealias tp match { @@ -241,41 +242,41 @@ object GenericSignatures { val erasedUnderlying = fullErasure(ref.underlying.bounds.hi) // don't emit type param name if the param is upper-bounded by a primitive type (including via a value class) if erasedUnderlying.isPrimitiveValueType then - jsig(erasedUnderlying, toplevel, unboxedVCs) + jsig(erasedUnderlying, toplevel = toplevel, unboxedVCs = unboxedVCs) else typeParamSig(ref.paramName.lastPart) case defn.ArrayOf(elemtp) => if (isGenericArrayElement(elemtp, isScala2 = false)) - jsig(defn.ObjectType) + jsig1(defn.ObjectType) else builder.append(ClassfileConstants.ARRAY_TAG) elemtp match - case TypeBounds(lo, hi) => jsig(hi.widenDealias) - case _ => jsig(elemtp) + case TypeBounds(lo, hi) => jsig1(hi.widenDealias) + case _ => jsig1(elemtp) case RefOrAppliedType(sym, pre, args) => if (sym == defn.PairClass && tupleArity(tp) > Definitions.MaxTupleArity) - jsig(defn.TupleXXLClass.typeRef) + jsig1(defn.TupleXXLClass.typeRef) else if (isTypeParameterInSig(sym, sym0)) { assert(!sym.isAliasType, "Unexpected alias type: " + sym) typeParamSig(sym.name.lastPart) } else if (defn.specialErasure.contains(sym)) - jsig(defn.specialErasure(sym).nn.typeRef) + jsig1(defn.specialErasure(sym).nn.typeRef) else if (sym == defn.UnitClass || sym == defn.BoxedUnitModule) - jsig(defn.BoxedUnitClass.typeRef) + jsig1(defn.BoxedUnitClass.typeRef) else if (sym == defn.NothingClass) builder.append("Lscala/runtime/Nothing$;") else if (sym == defn.NullClass) builder.append("Lscala/runtime/Null$;") else if (sym.isPrimitiveValueClass) - if (!unboxedVCs) jsig(defn.ObjectType) - else if (sym == defn.UnitClass) jsig(defn.BoxedUnitClass.typeRef) + if (!unboxedVCs) jsig1(defn.ObjectType) + else if (sym == defn.UnitClass) jsig1(defn.BoxedUnitClass.typeRef) else builder.append(defn.typeTag(sym.info)) else if (sym.isDerivedValueClass) { if (unboxedVCs) { val erasedUnderlying = fullErasure(tp) - jsig(erasedUnderlying, toplevel) + jsig(erasedUnderlying, toplevel = toplevel, unboxedVCs = true) } else classSig(sym, pre, args) } else if (defn.isSyntheticFunctionClass(sym)) { @@ -285,14 +286,14 @@ object GenericSignatures { else if sym.isClass then classSig(sym, pre, args) else - jsig(erasure(tp), toplevel, unboxedVCs) + jsig(erasure(tp), toplevel = toplevel, unboxedVCs = unboxedVCs) case ExprType(restpe) if toplevel => builder.append("()") methodResultSig(restpe) case ExprType(restpe) => - jsig(defn.FunctionType(0).appliedTo(restpe)) + jsig1(defn.FunctionType(0).appliedTo(restpe)) case PolyType(tparams, mtpe: MethodType) => assert(tparams.nonEmpty) @@ -320,7 +321,7 @@ object GenericSignatures { builder.append('(') // TODO: Update once we support varargs params.foreach { tp => - jsig(tp) + jsig1(tp) } builder.append(')') methodResultSig(restpe) @@ -338,7 +339,7 @@ object GenericSignatures { val (reprParents, _) = splitIntersection(parents) val repr = reprParents.find(_.typeSymbol.is(TypeParam)).getOrElse(reprParents.head) - jsig(repr, unboxedVCs = unboxedVCs) + jsig(repr, toplevel = false, unboxedVCs = unboxedVCs) case ci: ClassInfo => val tParams = tp.typeParams diff --git a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala index d05b5044ff5c..543c364439a5 100644 --- a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala +++ b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala @@ -145,6 +145,31 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: val span = pos.span.toSynthetic invokeCall(statementId, span) + private def transformApplyArgs(trees: List[Tree])(using Context): List[Tree] = + if allConstArgs(trees) then trees else transform(trees) + + private def transformInnerApply(tree: Tree)(using Context): Tree = tree match + case a: Apply if a.fun.symbol == defn.StringContextModule_apply => + a + case a: Apply => + cpy.Apply(a)( + transformInnerApply(a.fun), + transformApplyArgs(a.args) + ) + case a: TypeApply => + cpy.TypeApply(a)( + transformInnerApply(a.fun), + transformApplyArgs(a.args) + ) + case s: Select => + cpy.Select(s)(transformInnerApply(s.qualifier), s.name) + case i: (Ident | This) => i + case t: Typed => + cpy.Typed(t)(transformInnerApply(t.expr), t.tpt) + case other => transform(other) + + private def allConstArgs(args: List[Tree]) = + args.forall(arg => arg.isInstanceOf[Literal] || arg.isInstanceOf[Ident]) /** * Tries to instrument an `Apply`. * These "tryInstrument" methods are useful to tweak the generation of coverage instrumentation, @@ -158,10 +183,12 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: // Create a call to Invoker.invoked(coverageDirectory, newStatementId) val coverageCall = createInvokeCall(tree, tree.sourcePos) - if needsLift(tree) then - // Transform args and fun, i.e. instrument them if needed (and if possible) - val app = cpy.Apply(tree)(transform(tree.fun), tree.args.map(transform)) + // Transform args and fun, i.e. instrument them if needed (and if possible) + val app = + if tree.fun.symbol eq defn.throwMethod then tree + else cpy.Apply(tree)(transformInnerApply(tree.fun), transformApplyArgs(tree.args)) + if needsLift(tree) then // Lifts the arguments. Note that if only one argument needs to be lifted, we lift them all. // Also, tree.fun can be lifted too. // See LiftCoverage for the internal working of this lifting. @@ -171,11 +198,10 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: InstrumentedParts(liftedDefs.toList, coverageCall, liftedApp) else // Instrument without lifting - val transformed = cpy.Apply(tree)(transform(tree.fun), transform(tree.args)) - InstrumentedParts.singleExpr(coverageCall, transformed) + InstrumentedParts.singleExpr(coverageCall, app) else // Transform recursively but don't instrument the tree itself - val transformed = cpy.Apply(tree)(transform(tree.fun), transform(tree.args)) + val transformed = cpy.Apply(tree)(transformInnerApply(tree.fun), transform(tree.args)) InstrumentedParts.notCovered(transformed) private def tryInstrument(tree: Ident)(using Context): InstrumentedParts = @@ -187,9 +213,14 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: else InstrumentedParts.notCovered(tree) + private def tryInstrument(tree: Literal)(using Context): InstrumentedParts = + val coverageCall = createInvokeCall(tree, tree.sourcePos) + InstrumentedParts.singleExpr(coverageCall, tree) + private def tryInstrument(tree: Select)(using Context): InstrumentedParts = val sym = tree.symbol - val transformed = cpy.Select(tree)(transform(tree.qualifier), tree.name) + val qual = transform(tree.qualifier).ensureConforms(tree.qualifier.tpe) + val transformed = cpy.Select(tree)(qual, tree.name) if canInstrumentParameterless(sym) then // call to a parameterless method val coverageCall = createInvokeCall(tree, tree.sourcePos) @@ -202,6 +233,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: tree match case t: Apply => tryInstrument(t) case t: Ident => tryInstrument(t) + case t: Literal => tryInstrument(t) case t: Select => tryInstrument(t) case _ => InstrumentedParts.notCovered(transform(tree)) @@ -223,10 +255,14 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: inContext(transformCtx(tree)) { // necessary to position inlined code properly tree match // simple cases - case tree: (Import | Export | Literal | This | Super | New) => tree + case tree: (Import | Export | This | Super | New) => tree case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident (referring to a type), TypeTree, ... case tree if !tree.span.exists || tree.span.isZeroExtent => tree // no meaningful position + case tree: Literal => + val rest = tryInstrument(tree).toTree + rest + // identifier case tree: Ident => tryInstrument(tree).toTree @@ -280,6 +316,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: case tree: CaseDef => transformCaseDef(tree) + case tree: ValDef if tree.symbol.is(Inline) => + tree // transforming inline vals will result in `inline value must be pure` errors + case tree: ValDef => // only transform the rhs val rhs = transform(tree.rhs) @@ -323,13 +362,13 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: ) case tree: Inlined => - // Ideally, tree.call would provide precise information about the inlined call, - // and we would use this information for the coverage report. - // But PostTyper simplifies tree.call, so we can't report the actual method that was inlined. - // In any case, the subtrees need to be repositioned right now, otherwise the - // coverage statement will point to a potentially unreachable source file. - val dropped = Inlines.dropInlined(tree) // drop and reposition - transform(dropped) // transform the content of the Inlined + // Inlined code contents might come from another file (or project), + // which means that we cannot clearly designate which part of the inlined code + // was run using the API we are given. + // At best, we can show that the Inlined tree itself was reached. + // Additionally, Scala 2's coverage ignores macro calls entirely, + // so let's do that here too, also for regular inlined calls. + tree // For everything else just recurse and transform case _ => @@ -559,15 +598,14 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: private def isCompilerIntrinsicMethod(sym: Symbol)(using Context): Boolean = val owner = sym.maybeOwner owner.exists && ( - owner.eq(defn.AnyClass) || - owner.isPrimitiveValueClass || + (owner.eq(defn.AnyClass) && (sym == defn.Any_asInstanceOf || sym == defn.Any_isInstanceOf)) || owner.maybeOwner == defn.CompiletimePackageClass ) object InstrumentCoverage: val name: String = "instrumentCoverage" val description: String = "instrument code for coverage checking" - val ExcludeMethodFlags: FlagSet = Synthetic | Artifact | Erased + val ExcludeMethodFlags: FlagSet = Artifact | Erased /** * An instrumented Tree, in 3 parts. diff --git a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala index 3fbdf1fa5427..12045be7419a 100644 --- a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala +++ b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala @@ -34,8 +34,8 @@ object LambdaLift: val liftedDefs: HashMap[Symbol, ListBuffer[Tree]] = new HashMap val deps = new Dependencies(ctx.compilationUnit.tpdTree, ctx.withPhase(thisPhase)): - def isExpr(sym: Symbol)(using Context): Boolean = sym.is(Method) - def enclosure(using Context) = ctx.owner.enclosingMethod + def isExpr(sym: Symbol)(using Context): Boolean = sym.is(Method) || sym.hasAnnotation(defn.ScalaStaticAnnot) + def enclosure(using Context) = ctx.owner.enclosingMethodOrStatic override def process(tree: Tree)(using Context): Unit = super.process(tree) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 72a3ff7324e8..6d5175bf17a0 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -193,7 +193,11 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase ++ sym.annotations) else if sym.is(Param) then + // @unused is getter/setter but we want it on ordinary method params + // @param should be consulted only for fields + val unusing = sym.getAnnotation(defn.UnusedAnnot) sym.keepAnnotationsCarrying(thisPhase, Set(defn.ParamMetaAnnot), orNoneOf = defn.NonBeanMetaAnnots) + unusing.foreach(sym.addAnnotation) else if sym.is(ParamAccessor) then sym.keepAnnotationsCarrying(thisPhase, Set(defn.GetterMetaAnnot, defn.FieldMetaAnnot)) else diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 2eebc2347eec..c727a65969c5 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -149,7 +149,7 @@ abstract class Recheck extends Phase, SymTransformer: * can be retrieved with `knownType` */ private val keepAllTypes = inContext(ictx) { - ictx.settings.Xprint.value.containsPhase(thisPhase) + ictx.settings.Vprint.value.containsPhase(thisPhase) } /** Should type of `tree` be kept in an attachment so that it can be retrieved with diff --git a/compiler/src/dotty/tools/dotc/transform/Splicer.scala b/compiler/src/dotty/tools/dotc/transform/Splicer.scala index 7a1992b0ba18..a52b78682ac1 100644 --- a/compiler/src/dotty/tools/dotc/transform/Splicer.scala +++ b/compiler/src/dotty/tools/dotc/transform/Splicer.scala @@ -54,7 +54,7 @@ object Splicer { inContext(sliceContext) { val oldContextClassLoader = Thread.currentThread().getContextClassLoader Thread.currentThread().setContextClassLoader(classLoader) - try ctx.profiler.onMacroSplice(owner){ + try { val interpreter = new SpliceInterpreter(splicePos, classLoader) // Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree diff --git a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala index 70f82434a5a6..f66cbbcf58b0 100644 --- a/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala +++ b/compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala @@ -15,6 +15,7 @@ import util.Property import util.Spans.Span import config.Printers.derive import NullOpsDecorator.* +import scala.runtime.Statics object SyntheticMembers { @@ -67,7 +68,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { myCaseSymbols = defn.caseClassSynthesized myCaseModuleSymbols = myCaseSymbols.filter(_ ne defn.Any_equals) myEnumValueSymbols = List(defn.Product_productPrefix) - myNonJavaEnumValueSymbols = myEnumValueSymbols :+ defn.Any_toString :+ defn.Enum_ordinal + myNonJavaEnumValueSymbols = myEnumValueSymbols :+ defn.Any_toString :+ defn.Enum_ordinal :+ defn.Any_hashCode } def valueSymbols(using Context): List[Symbol] = { initSymbols; myValueSymbols } @@ -95,6 +96,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { val isSimpleEnumValue = isEnumValue && !clazz.owner.isAllOf(EnumCase) val isJavaEnumValue = isEnumValue && clazz.derivesFrom(defn.JavaEnumClass) val isNonJavaEnumValue = isEnumValue && !isJavaEnumValue + val ownName = clazz.name.stripModuleClassSuffix.toString val symbolsToSynthesize: List[Symbol] = if clazz.is(Case) then @@ -108,6 +110,12 @@ class SyntheticMembers(thisPhase: DenotTransformer) { def syntheticDefIfMissing(sym: Symbol): List[Tree] = if (existingDef(sym, clazz).exists) Nil else syntheticDef(sym) :: Nil + def identifierRef: Tree = + if isSimpleEnumValue then // owner is `def $new(_$ordinal: Int, $name: String) = new MyEnum { ... }` + ref(clazz.owner.paramSymss.head.find(_.name == nme.nameDollar).get) + else // assume owner is `val Foo = new MyEnum { def ordinal = 0 }` + Literal(Constant(clazz.owner.name.toString)) + def syntheticDef(sym: Symbol): Tree = { val synthetic = sym.copy( owner = clazz, @@ -118,8 +126,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { def forwardToRuntime(vrefs: List[Tree]): Tree = ref(defn.runtimeMethodRef("_" + sym.name.toString)).appliedToTermArgs(This(clazz) :: vrefs) - def ownName: Tree = - Literal(Constant(clazz.name.stripModuleClassSuffix.toString)) + def ownNameLit: Tree = Literal(Constant(ownName)) def nameRef: Tree = if isJavaEnumValue then @@ -128,12 +135,6 @@ class SyntheticMembers(thisPhase: DenotTransformer) { else identifierRef - def identifierRef: Tree = - if isSimpleEnumValue then // owner is `def $new(_$ordinal: Int, $name: String) = new MyEnum { ... }` - ref(clazz.owner.paramSymss.head.find(_.name == nme.nameDollar).get) - else // assume owner is `val Foo = new MyEnum { def ordinal = 0 }` - Literal(Constant(clazz.owner.name.toString)) - def ordinalRef: Tree = if isSimpleEnumValue then // owner is `def $new(_$ordinal: Int, $name: String) = new MyEnum { ... }` ref(clazz.owner.paramSymss.head.find(_.name == nme.ordinalDollar_).get) @@ -146,7 +147,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { Literal(Constant(candidate.get)) def toStringBody(vrefss: List[List[Tree]]): Tree = - if (clazz.is(ModuleClass)) ownName + if (clazz.is(ModuleClass)) ownNameLit else if (isNonJavaEnumValue) identifierRef else forwardToRuntime(vrefss.head) @@ -159,7 +160,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { case nme.ordinal => ordinalRef case nme.productArity => Literal(Constant(accessors.length)) case nme.productPrefix if isEnumValue => nameRef - case nme.productPrefix => ownName + case nme.productPrefix => ownNameLit case nme.productElement => productElementBody(accessors.length, vrefss.head.head) case nme.productElementName => productElementNameBody(accessors.length, vrefss.head.head) } @@ -300,39 +301,37 @@ class SyntheticMembers(thisPhase: DenotTransformer) { ref(accessors.head).select(nme.hashCode_).ensureApplied } - /** The class - * - * ``` - * case object C - * ``` + /** + * A `case object C` or a `case class C()` without parameters gets the `hashCode` method + * ``` + * def hashCode: Int = "C".hashCode // constant folded + * ``` * - * gets the `hashCode` method: + * Otherwise, if none of the parameters are primitive types: + * ``` + * def hashCode: Int = MurmurHash3.productHash( + * this, + * Statics.mix(0xcafebabe, "C".hashCode), // constant folded + * ignorePrefix = true) + * ``` * - * ``` - * def hashCode: Int = "C".hashCode // constant folded - * ``` - * - * The class - * - * ``` - * case class C(x: T, y: U) - * ``` - * - * if none of `T` or `U` are primitive types, gets the `hashCode` method: - * - * ``` - * def hashCode: Int = ScalaRunTime._hashCode(this) - * ``` + * The implementation used to invoke `ScalaRunTime._hashCode`, but that implementation mixes in the result + * of `productPrefix`, which causes scala/bug#13033. By setting `ignorePrefix = true` and mixing in the case + * name into the seed, the bug can be fixed and the generated code works with the unchanged Scala library. * - * else if either `T` or `U` are primitive, gets the `hashCode` method implemented by [[caseHashCodeBody]] + * For case classes with primitive paramters, see [[caseHashCodeBody]]. */ def chooseHashcode(using Context) = - if (clazz.is(ModuleClass)) - Literal(Constant(clazz.name.stripModuleClassSuffix.toString.hashCode)) + if (isNonJavaEnumValue) identifierRef.select(nme.hashCode_).appliedToTermArgs(Nil) + else if (accessors.isEmpty) Literal(Constant(ownName.hashCode)) else if (accessors.exists(_.info.finalResultType.classSymbol.isPrimitiveValueClass)) caseHashCodeBody else - ref(defn.ScalaRuntime__hashCode).appliedTo(This(clazz)) + ref(defn.MurmurHash3Module).select(defn.MurmurHash3_productHash).appliedTo( + This(clazz), + Literal(Constant(Statics.mix(0xcafebabe, ownName.hashCode))), + Literal(Constant(true)) + ) /** The class * @@ -345,7 +344,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { * ``` * def hashCode: Int = { * var acc: Int = 0xcafebabe - * acc = Statics.mix(acc, this.productPrefix.hashCode()); + * acc = Statics.mix(acc, "C".hashCode); * acc = Statics.mix(acc, x); * acc = Statics.mix(acc, Statics.this.anyHash(y)); * Statics.finalizeHash(acc, 2) @@ -356,7 +355,7 @@ class SyntheticMembers(thisPhase: DenotTransformer) { val acc = newSymbol(ctx.owner, nme.acc, Mutable | Synthetic, defn.IntType, coord = ctx.owner.span) val accDef = ValDef(acc, Literal(Constant(0xcafebabe))) val mixPrefix = Assign(ref(acc), - ref(defn.staticsMethod("mix")).appliedTo(ref(acc), This(clazz).select(defn.Product_productPrefix).select(defn.Any_hashCode).appliedToNone)) + ref(defn.staticsMethod("mix")).appliedTo(ref(acc), Literal(Constant(ownName.hashCode)))) val mixes = for (accessor <- accessors) yield Assign(ref(acc), ref(defn.staticsMethod("mix")).appliedTo(ref(acc), hashImpl(accessor))) val finish = ref(defn.staticsMethod("finalizeHash")).appliedTo(ref(acc), Literal(Constant(accessors.size))) diff --git a/compiler/src/dotty/tools/dotc/transform/TailRec.scala b/compiler/src/dotty/tools/dotc/transform/TailRec.scala index 158b72f7abdb..b2be8d4651f0 100644 --- a/compiler/src/dotty/tools/dotc/transform/TailRec.scala +++ b/compiler/src/dotty/tools/dotc/transform/TailRec.scala @@ -4,9 +4,9 @@ package transform import ast.{TreeTypeMap, tpd} import config.Printers.tailrec import core.* -import Contexts.*, Flags.*, Symbols.*, Decorators.em +import Contexts.*, Flags.*, Symbols.*, Decorators.* import Constants.Constant -import NameKinds.{TailLabelName, TailLocalName, TailTempName} +import NameKinds.{DefaultGetterName, TailLabelName, TailLocalName, TailTempName} import StdNames.nme import reporting.* import transform.MegaPhase.MiniPhase @@ -194,7 +194,8 @@ class TailRec extends MiniPhase { def isInfiniteRecCall(tree: Tree): Boolean = { def tailArgOrPureExpr(stat: Tree): Boolean = stat match { case stat: ValDef if stat.name.is(TailTempName) || !stat.symbol.is(Mutable) => tailArgOrPureExpr(stat.rhs) - case Assign(lhs: Ident, rhs) if lhs.symbol.name.is(TailLocalName) => tailArgOrPureExpr(rhs) + case Assign(lhs: Ident, rhs) if lhs.symbol.name.is(TailLocalName) => + tailArgOrPureExpr(rhs) || varForRewrittenThis.exists(_ == lhs.symbol && rhs.tpe.isStable) case Assign(lhs: Ident, rhs: Ident) => lhs.symbol == rhs.symbol case stat: Ident if stat.symbol.name.is(TailLocalName) => true case _ => tpd.isPureExpr(stat) @@ -322,7 +323,14 @@ class TailRec extends MiniPhase { method.matches(calledMethod) && enclosingClass.appliedRef.widen <:< prefix.tpe.widenDealias - if (isRecursiveCall) + if isRecursiveCall then + if ctx.settings.Whas.recurseWithDefault then + tree.args.find(_.symbol.name.is(DefaultGetterName)) match + case Some(arg) => + val DefaultGetterName(_, index) = arg.symbol.name: @unchecked + report.warning(RecurseWithDefault(calledMethod.info.firstParamNames(index)), tree.srcPos) + case _ => + if (inTailPosition) { tailrec.println("Rewriting tail recursive call: " + tree.span) rewrote = true @@ -343,6 +351,12 @@ class TailRec extends MiniPhase { case prefix: This if prefix.symbol == enclosingClass => // Avoid assigning `this = this` assignParamPairs + case prefix + if prefix.symbol.is(Module) + && prefix.symbol.moduleClass == enclosingClass + && isPurePath(prefix) => + // Avoid assigning `this = MyObject` + assignParamPairs case _ => (getVarForRewrittenThis(), noTailTransform(prefix)) :: assignParamPairs diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index 33d1ea20df14..9d29344789c3 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -214,7 +214,7 @@ object TreeChecker { private val everDefinedSyms = MutableSymbolMap[untpd.Tree]() // don't check value classes after typer, as the constraint about constructors doesn't hold after transform - override def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = () + override def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = () def withDefinedSyms[T](trees: List[untpd.Tree])(op: => T)(using Context): T = { var locally = List.empty[Symbol] diff --git a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala index 4548dccb598f..a9115251e14f 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Semantic.scala @@ -378,7 +378,7 @@ object Semantic: // ----- Checker State ----------------------------------- /** The state that threads through the interpreter */ - type Contextual[T] = (Context, Trace, Promoted, Cache.Data, Reporter) ?=> T + type Contextual[T] = (Context, Trace, Promoted, Cache.Data, Reporter, TreeCache.CacheData) ?=> T // ----- Error Handling ----------------------------------- @@ -443,6 +443,43 @@ object Semantic: inline def reporter(using r: Reporter): Reporter = r +// ----- Cache for Trees ----------------------------- + + object TreeCache: + class CacheData: + private val emptyTrees = mutable.Set[ValOrDefDef]() + private val templatesToSkip = mutable.Set[Template]() + + def checkTemplateBodyValidity(tpl: Template, className: String)(using Context): Unit = + if (templatesToSkip.contains(tpl)) + throw new TastyTreeException(className) + + val errorCount = ctx.reporter.errorCount + tpl.forceFields() + + if (ctx.reporter.errorCount > errorCount) + templatesToSkip.add(tpl) + throw new TastyTreeException(className) + + extension (tree: ValOrDefDef) + def getRhs(using Context): Tree = + def getTree: Tree = + val errorCount = ctx.reporter.errorCount + val rhs = tree.rhs + + if (ctx.reporter.errorCount > errorCount) + emptyTrees.add(tree) + report.warning("Ignoring analyses of " + tree.name + " due to error in reading TASTy.") + EmptyTree + else + rhs + + if (emptyTrees.contains(tree)) EmptyTree + else getTree + end TreeCache + + inline def treeCache(using t: TreeCache.CacheData): TreeCache.CacheData = t + // ----- Operations on domains ----------------------------- extension (a: Value) def join(b: Value): Value = @@ -562,7 +599,7 @@ object Semantic: case ref: Ref => val target = if needResolve then resolve(ref.klass, field) else field if target.is(Flags.Lazy) then - val rhs = target.defTree.asInstanceOf[ValDef].rhs + val rhs = target.defTree.asInstanceOf[ValDef].getRhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else if target.exists then val obj = ref.objekt @@ -577,7 +614,7 @@ object Semantic: // return `Hot` here, errors are reported in checking `ThisRef` Hot else if target.hasSource then - val rhs = target.defTree.asInstanceOf[ValOrDefDef].rhs + val rhs = target.defTree.asInstanceOf[ValOrDefDef].getRhs eval(rhs, ref, target.owner.asClass, cacheResult = true) else val error = CallUnknown(field)(trace) @@ -622,6 +659,8 @@ object Semantic: val methodType = atPhaseBeforeTransforms { meth.info.stripPoly } var allArgsHot = true val allParamTypes = methodType.paramInfoss.flatten.map(_.repeatedToSingle) + if(allParamTypes.size != args.size) + report.warning("[Internal error] Number of parameters do not match number of arguments in " + meth.name) val errors = allParamTypes.zip(args).flatMap { (info, arg) => val tryReporter = Reporter.errorsIn { arg.promote } allArgsHot = allArgsHot && tryReporter.errors.isEmpty @@ -701,7 +740,7 @@ object Semantic: else reporter.reportAll(tryReporter.errors) extendTrace(ddef) { - eval(ddef.rhs, ref, cls, cacheResult = true) + eval(ddef.getRhs, ref, cls, cacheResult = true) } else if ref.canIgnoreMethodCall(target) then Hot @@ -768,7 +807,7 @@ object Semantic: val tpl = cls.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] extendTrace(cls.defTree) { init(tpl, ref, cls) } else - val initCall = ddef.rhs match + val initCall = ddef.getRhs match case Block(call :: _, _) => call case call => call extendTrace(ddef) { eval(initCall, ref, cls) } @@ -787,7 +826,7 @@ object Semantic: extendTrace(cls.defTree) { eval(tpl, ref, cls, cacheResult = true) } ref else - extendTrace(ddef) { eval(ddef.rhs, ref, cls, cacheResult = true) } + extendTrace(ddef) { eval(ddef.getRhs, ref, cls, cacheResult = true) } else if ref.canIgnoreMethodCall(ctor) then Hot else @@ -897,8 +936,7 @@ object Semantic: case Cold => Cold - case ref: Ref => eval(vdef.rhs, ref, enclosingClass, cacheResult = sym.is(Flags.Lazy)) - + case ref: Ref => eval(vdef.getRhs, ref, enclosingClass, cacheResult = sym.is(Flags.Lazy)) case _ => report.error("[Internal error] unexpected this value when accessing local variable, sym = " + sym.show + ", thisValue = " + thisValue2.show + Trace.show, Trace.position) Hot @@ -1105,7 +1143,7 @@ object Semantic: * * The class to be checked must be an instantiable concrete class. */ - private def checkClass(classSym: ClassSymbol)(using Cache.Data, Context): Unit = + private def checkClass(classSym: ClassSymbol)(using Cache.Data, Context, TreeCache.CacheData): Unit = val thisRef = ThisRef(classSym) val tpl = classSym.defTree.asInstanceOf[TypeDef].rhs.asInstanceOf[Template] @@ -1140,8 +1178,12 @@ object Semantic: */ def checkClasses(classes: List[ClassSymbol])(using Context): Unit = given Cache.Data() + given TreeCache.CacheData() for classSym <- classes if isConcreteClass(classSym) do - checkClass(classSym) + try + checkClass(classSym) + catch + case TastyTreeException(className) => report.warning("Skipping the analysis of " + classSym.show + " due to an error reading the body of " + className + "'s TASTy.") // ----- Semantic definition -------------------------------- type ArgInfo = TraceValue[Value] @@ -1298,7 +1340,7 @@ object Semantic: } case closureDef(ddef) => - Fun(ddef.rhs, thisV, klass) + Fun(ddef.getRhs, thisV, klass) case PolyFun(body) => Fun(body, thisV, klass) @@ -1353,7 +1395,7 @@ object Semantic: case vdef : ValDef => // local val definition - eval(vdef.rhs, thisV, klass) + eval(vdef.getRhs, thisV, klass) case ddef : DefDef => // local method @@ -1475,6 +1517,8 @@ object Semantic: * @param klass The class to which the template belongs. */ def init(tpl: Template, thisV: Ref, klass: ClassSymbol): Contextual[Value] = log("init " + klass.show, printer, (_: Value).show) { + treeCache.checkTemplateBodyValidity(tpl, klass.show) + val paramsMap = tpl.constr.termParamss.flatten.map { vdef => vdef.name -> thisV.objekt.field(vdef.symbol) }.toMap @@ -1571,8 +1615,8 @@ object Semantic: // class body if thisV.isThisRef || !thisV.asInstanceOf[Warm].isPopulatingParams then tpl.body.foreach { - case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.rhs.isEmpty => - val res = eval(vdef.rhs, thisV, klass) + case vdef : ValDef if !vdef.symbol.is(Flags.Lazy) && !vdef.getRhs.isEmpty => + val res = eval(vdef.getRhs, thisV, klass) // TODO: Improve promotion to avoid handling enum initialization specially // // The failing case is tests/init/pos/i12544.scala due to promotion failure. diff --git a/compiler/src/dotty/tools/dotc/transform/init/Util.scala b/compiler/src/dotty/tools/dotc/transform/init/Util.scala index ba2216504aef..a4741e276188 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Util.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Util.scala @@ -15,6 +15,9 @@ import config.Printers.init as printer import Trace.* object Util: + /** Exception used for errors encountered when reading TASTy. */ + case class TastyTreeException(msg: String) extends RuntimeException(msg) + /** Utility definition used for better error-reporting of argument errors */ case class TraceValue[T](value: T, trace: Trace) @@ -39,6 +42,8 @@ object Util: case Apply(fn, args) => val argTps = fn.tpe.widen match case mt: MethodType => mt.paramInfos + if (args.size != argTps.size) + report.warning("[Internal error] Number of arguments do not match number of argument types in " + tree.symbol.name) val normArgs: List[Arg] = args.zip(argTps).map { case (arg, _: ExprType) => ByNameArg(arg) case (arg, _) => arg diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala index 9e40792895c0..f152ac9a6876 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/FormatChecker.scala @@ -3,18 +3,16 @@ package transform.localopt import scala.annotation.tailrec import scala.collection.mutable.ListBuffer -import scala.util.chaining.* import scala.util.matching.Regex.Match - -import PartialFunction.cond - import dotty.tools.dotc.ast.tpd.{Match => _, *} import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.core.Phases.typerPhase +import dotty.tools.dotc.reporting.BadFormatInterpolation import dotty.tools.dotc.util.Spans.Span +import dotty.tools.dotc.util.chaining.* /** Formatter string checker. */ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List[Tree])(using Context): @@ -30,8 +28,9 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List def argType(argi: Int, types: Type*): Type = require(argi < argc, s"$argi out of range picking from $types") val tpe = argTypes(argi) - types.find(t => argConformsTo(argi, tpe, t)) - .orElse(types.find(t => argConvertsTo(argi, tpe, t))) + types.find(t => t != defn.AnyType && argConformsTo(argi, tpe, t)) + .orElse(types.find(t => t != defn.AnyType && argConvertsTo(argi, tpe, t))) + .orElse(types.find(t => t == defn.AnyType && argConformsTo(argi, tpe, t))) .getOrElse { report.argError(s"Found: ${tpe.show}, Required: ${types.map(_.show).mkString(", ")}", argi) actuals += args(argi) @@ -64,50 +63,57 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List /** For N part strings and N-1 args to interpolate, normalize parts and check arg types. * - * Returns normalized part strings and args, where args correcpond to conversions in tail of parts. + * Returns normalized part strings and args, where args correspond to conversions in tail of parts. */ def checked: (List[String], List[Tree]) = val amended = ListBuffer.empty[String] val convert = ListBuffer.empty[Conversion] + def checkPart(part: String, n: Int): Unit = + val matches = formatPattern.findAllMatchIn(part) + + def insertStringConversion(): Unit = + amended += "%s" + part + val cv = Conversion.stringXn(n) + cv.accepts(argType(n-1, defn.AnyType)) + convert += cv + cv.lintToString(argTypes(n-1)) + + def errorLeading(op: Conversion) = op.errorAt(Spec): + s"conversions must follow a splice; ${Conversion.literalHelp}" + + def accept(op: Conversion): Unit = + if !op.isLeading then errorLeading(op) + op.accepts(argType(n-1, op.acceptableVariants*)) + amended += part + convert += op + op.lintToString(argTypes(n-1)) + + // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed + if n == 0 then amended += part + else if !matches.hasNext then insertStringConversion() + else + val cv = Conversion(matches.next(), n) + if cv.isLiteral then insertStringConversion() + else if cv.isIndexed then + if cv.index.getOrElse(-1) == n then accept(cv) else insertStringConversion() + else if !cv.isError then accept(cv) + + // any remaining conversions in this part must be either literals or indexed + while matches.hasNext do + val cv = Conversion(matches.next(), n) + if n == 0 && cv.hasFlag('<') then cv.badFlag('<', "No last arg") + else if !cv.isLiteral && !cv.isIndexed then errorLeading(cv) + end checkPart + @tailrec - def loop(remaining: List[String], n: Int): Unit = - remaining match - case part0 :: more => - def badPart(t: Throwable): String = "".tap(_ => report.partError(t.getMessage.nn, index = n, offset = 0)) - val part = try StringContext.processEscapes(part0) catch badPart - val matches = formatPattern.findAllMatchIn(part) - - def insertStringConversion(): Unit = - amended += "%s" + part - convert += Conversion(formatPattern.findAllMatchIn("%s").next(), n) // improve - argType(n-1, defn.AnyType) - def errorLeading(op: Conversion) = op.errorAt(Spec)(s"conversions must follow a splice; ${Conversion.literalHelp}") - def accept(op: Conversion): Unit = - if !op.isLeading then errorLeading(op) - op.accepts(argType(n-1, op.acceptableVariants*)) - amended += part - convert += op - - // after the first part, a leading specifier is required for the interpolated arg; %s is supplied if needed - if n == 0 then amended += part - else if !matches.hasNext then insertStringConversion() - else - val cv = Conversion(matches.next(), n) - if cv.isLiteral then insertStringConversion() - else if cv.isIndexed then - if cv.index.getOrElse(-1) == n then accept(cv) else insertStringConversion() - else if !cv.isError then accept(cv) - - // any remaining conversions in this part must be either literals or indexed - while matches.hasNext do - val cv = Conversion(matches.next(), n) - if n == 0 && cv.hasFlag('<') then cv.badFlag('<', "No last arg") - else if !cv.isLiteral && !cv.isIndexed then errorLeading(cv) - - loop(more, n + 1) - case Nil => () - end loop + def loop(remaining: List[String], n: Int): Unit = remaining match + case part0 :: remaining => + def badPart(t: Throwable): String = "".tap(_ => report.partError(t.getMessage.nn, index = n, offset = 0)) + val part = try StringContext.processEscapes(part0) catch badPart + checkPart(part, n) + loop(remaining, n + 1) + case Nil => loop(parts, n = 0) if reported then (Nil, Nil) @@ -125,10 +131,8 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List def intOf(g: SpecGroup): Option[Int] = group(g).map(_.toInt) extension (inline value: Boolean) - inline def or(inline body: => Unit): Boolean = value || { body ; false } - inline def orElse(inline body: => Unit): Boolean = value || { body ; true } - inline def and(inline body: => Unit): Boolean = value && { body ; true } - inline def but(inline body: => Unit): Boolean = value && { body ; false } + inline infix def or(inline body: => Unit): Boolean = value || { body; false } + inline infix def and(inline body: => Unit): Boolean = value && { body; true } enum Kind: case StringXn, HashXn, BooleanXn, CharacterXn, IntegralXn, FloatingPointXn, DateTimeXn, LiteralXn, ErrorXn @@ -147,9 +151,10 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List // the conversion char is the head of the op string (but see DateTimeXn) val cc: Char = kind match - case ErrorXn => if op.isEmpty then '?' else op(0) - case DateTimeXn => if op.length > 1 then op(1) else '?' - case _ => op(0) + case ErrorXn => if op.isEmpty then '?' else op(0) + case DateTimeXn => if op.length <= 1 then '?' else op(1) + case StringXn => if op.isEmpty then 's' else op(0) // accommodate the default %s + case _ => op(0) def isIndexed: Boolean = index.nonEmpty || hasFlag('<') def isError: Boolean = kind == ErrorXn @@ -209,18 +214,40 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List // is the specifier OK with the given arg def accepts(arg: Type): Boolean = kind match - case BooleanXn => arg == defn.BooleanType orElse warningAt(CC)("Boolean format is null test for non-Boolean") - case IntegralXn => - arg == BigIntType || !cond(cc) { - case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => "+ (".filter(hasFlag).foreach(bad => badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X")) ; true - } + case BooleanXn if arg != defn.BooleanType => + warningAt(CC): + """non-Boolean value formats as "true" for non-null references and boxed primitives, otherwise "false"""" + true + case IntegralXn if arg != BigIntType => + cc match + case 'o' | 'x' | 'X' if hasAnyFlag("+ (") => + "+ (".filter(hasFlag).foreach: bad => + badFlag(bad, s"only use '$bad' for BigInt conversions to o, x, X") + false case _ => true + case _ => true + + def lintToString(arg: Type): Unit = + def checkIsStringify(tp: Type): Boolean = tp.widen match + case OrType(tp1, tp2) => + checkIsStringify(tp1) || checkIsStringify(tp2) + case tp => + !(tp =:= defn.StringType) + && { + tp =:= defn.UnitType + && { warningAt(CC)("interpolated Unit value"); true } + || + !tp.isPrimitiveValueType + && { warningAt(CC)("interpolation uses toString"); true } + } + if ctx.settings.Whas.toStringInterpolated && kind == StringXn then + checkIsStringify(arg): Unit // what arg type if any does the conversion accept def acceptableVariants: List[Type] = kind match case StringXn => if hasFlag('#') then FormattableType :: Nil else defn.AnyType :: Nil - case BooleanXn => defn.BooleanType :: defn.NullType :: Nil + case BooleanXn => defn.BooleanType :: defn.NullType :: defn.AnyType :: Nil // warn if not boolean case HashXn => defn.AnyType :: Nil case CharacterXn => defn.CharType :: defn.ByteType :: defn.ShortType :: defn.IntType :: Nil case IntegralXn => defn.IntType :: defn.LongType :: defn.ByteType :: defn.ShortType :: BigIntType :: Nil @@ -249,25 +276,30 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List object Conversion: def apply(m: Match, i: Int): Conversion = - def kindOf(cc: Char) = cc match - case 's' | 'S' => StringXn - case 'h' | 'H' => HashXn - case 'b' | 'B' => BooleanXn - case 'c' | 'C' => CharacterXn - case 'd' | 'o' | - 'x' | 'X' => IntegralXn - case 'e' | 'E' | - 'f' | - 'g' | 'G' | - 'a' | 'A' => FloatingPointXn - case 't' | 'T' => DateTimeXn - case '%' | 'n' => LiteralXn - case _ => ErrorXn - end kindOf m.group(CC) match - case Some(cc) => new Conversion(m, i, kindOf(cc(0))).tap(_.verify) - case None => new Conversion(m, i, ErrorXn).tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp")) + case Some(cc) => + val xn = cc(0) match + case 's' | 'S' => StringXn + case 'h' | 'H' => HashXn + case 'b' | 'B' => BooleanXn + case 'c' | 'C' => CharacterXn + case 'd' | 'o' | + 'x' | 'X' => IntegralXn + case 'e' | 'E' | + 'f' | + 'g' | 'G' | + 'a' | 'A' => FloatingPointXn + case 't' | 'T' => DateTimeXn + case '%' | 'n' => LiteralXn + case _ => ErrorXn + new Conversion(m, i, xn) + .tap(_.verify) + case None => + new Conversion(m, i, ErrorXn) + .tap(_.errorAt(Spec)(s"Missing conversion operator in '${m.matched}'; $literalHelp")) end apply + // construct a default %s conversion + def stringXn(i: Int): Conversion = new Conversion(formatPattern.findAllMatchIn("%").next(), i, StringXn) val literalHelp = "use %% for literal %, %n for newline" end Conversion @@ -277,10 +309,16 @@ class TypedFormatChecker(partsElems: List[Tree], parts: List[String], args: List val pos = partsElems(index).sourcePos val bgn = pos.span.start + offset val fin = if end < 0 then pos.span.end else pos.span.start + end - pos.withSpan(Span(bgn, fin, bgn)) + pos.withSpan(Span(start = bgn, end = fin, point = bgn)) extension (r: report.type) - def argError(message: String, index: Int): Unit = r.error(message, args(index).srcPos).tap(_ => reported = true) - def partError(message: String, index: Int, offset: Int, end: Int = -1): Unit = r.error(message, partPosAt(index, offset, end)).tap(_ => reported = true) - def partWarning(message: String, index: Int, offset: Int, end: Int = -1): Unit = r.warning(message, partPosAt(index, offset, end)).tap(_ => reported = true) + def argError(message: String, index: Int): Unit = + r.error(BadFormatInterpolation(message), args(index).srcPos) + .tap(_ => reported = true) + def partError(message: String, index: Int, offset: Int, end: Int = -1): Unit = + r.error(BadFormatInterpolation(message), partPosAt(index, offset, end)) + .tap(_ => reported = true) + def partWarning(message: String, index: Int, offset: Int, end: Int): Unit = + r.warning(BadFormatInterpolation(message), partPosAt(index, offset, end)) + .tap(_ => reported = true) end TypedFormatChecker diff --git a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala index 7743054f5487..0bb1df9a21e1 100644 --- a/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala +++ b/compiler/src/dotty/tools/dotc/transform/localopt/StringInterpolatorOpt.scala @@ -96,16 +96,33 @@ class StringInterpolatorOpt extends MiniPhase: def mkConcat(strs: List[Literal], elems: List[Tree]): Tree = val stri = strs.iterator val elemi = elems.iterator - var result: Tree = stri.next + var result: Tree = stri.next() def concat(tree: Tree): Unit = result = result.select(defn.String_+).appliedTo(tree).withSpan(tree.span) while elemi.hasNext do - concat(elemi.next) - val str = stri.next + val elem = elemi.next() + lintToString(elem) + concat(elem) + val str = stri.next() if !str.const.stringValue.isEmpty then concat(str) result end mkConcat + def lintToString(t: Tree): Unit = + def checkIsStringify(tp: Type): Boolean = tp.widen match + case OrType(tp1, tp2) => + checkIsStringify(tp1) || checkIsStringify(tp2) + case tp => + !(tp =:= defn.StringType) + && { + tp =:= defn.UnitType + && { report.warning("interpolated Unit value", t.srcPos); true } + || + !tp.isPrimitiveValueType + && { report.warning("interpolation uses toString", t.srcPos); true } + } + if ctx.settings.Whas.toStringInterpolated then + checkIsStringify(t.tpe): Unit val sym = tree.symbol // Test names first to avoid loading scala.StringContext if not used, and common names first val isInterpolatedMethod = diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 9cf94877328b..def5eaec708e 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -460,8 +460,8 @@ object SpaceEngine { val inArray = tycon.isRef(defn.ArrayClass) || tp.translucentSuperType.isRef(defn.ArrayClass) val args2 = if isTyped && !inArray then args.map(_ => WildcardType) - else args.map(arg => erase(arg, inArray = inArray, isValue = false)) - tp.derivedAppliedType(erase(tycon, inArray, isValue = false), args2) + else args.map(arg => erase(arg, inArray = inArray, isValue = false, isTyped = false)) + tp.derivedAppliedType(erase(tycon, inArray = inArray, isValue = false, isTyped = false), args2) case tp @ OrType(tp1, tp2) => OrType(erase(tp1, inArray, isValue), erase(tp2, inArray, isValue), tp.isSoft) @@ -642,49 +642,37 @@ object SpaceEngine { // we get // <== refineUsingParent(NatT, class Succ, []) = Succ[NatT] // <== isSub(Succ[NatT] <:< Succ[Succ[]]) = false - def getAppliedClass(tp: Type): (Type, List[Type]) = tp match - case tp @ AppliedType(_: HKTypeLambda, _) => (tp, Nil) - case tp @ AppliedType(tycon: TypeRef, _) if tycon.symbol.isClass => (tp, tp.args) + def getAppliedClass(tp: Type): Type = tp match + case tp @ AppliedType(_: HKTypeLambda, _) => tp + case tp @ AppliedType(tycon: TypeRef, _) if tycon.symbol.isClass => tp case tp @ AppliedType(tycon: TypeProxy, _) => getAppliedClass(tycon.superType.applyIfParameterized(tp.args)) - case tp => (tp, Nil) - val (tp, typeArgs) = getAppliedClass(tpOriginal) - // This function is needed to get the arguments of the types that will be applied to the class. - // This is necessary because if the arguments of the types contain Nothing, - // then this can affect whether the class will be taken into account during the exhaustiveness check - def getTypeArgs(parent: Symbol, child: Symbol, typeArgs: List[Type]): List[Type] = - val superType = child.typeRef.superType - if typeArgs.exists(_.isBottomType) && superType.isInstanceOf[ClassInfo] then - val parentClass = superType.asInstanceOf[ClassInfo].declaredParents.find(_.classSymbol == parent).get - val paramTypeMap = Map.from(parentClass.argTypes.map(_.typeSymbol).zip(typeArgs)) - val substArgs = child.typeRef.typeParamSymbols.map(param => paramTypeMap.getOrElse(param, WildcardType)) - substArgs - else Nil - def getChildren(sym: Symbol, typeArgs: List[Type]): List[Symbol] = + case tp => tp + val tp = getAppliedClass(tpOriginal) + def getChildren(sym: Symbol): List[Symbol] = sym.children.flatMap { child => if child eq sym then List(sym) // i3145: sealed trait Baz, val x = new Baz {}, Baz.children returns Baz... else if tp.classSymbol == defn.TupleClass || tp.classSymbol == defn.NonEmptyTupleClass then List(child) // TupleN and TupleXXL classes are used for Tuple, but they aren't Tuple's children - else if (child.is(Private) || child.is(Sealed)) && child.isOneOf(AbstractOrTrait) then - getChildren(child, getTypeArgs(sym, child, typeArgs)) - else - val childSubstTypes = child.typeRef.applyIfParameterized(getTypeArgs(sym, child, typeArgs)) - // if a class contains a field of type Nothing, - // then it can be ignored in pattern matching, because it is impossible to obtain an instance of it - val existFieldWithBottomType = childSubstTypes.fields.exists(_.info.isBottomType) - if existFieldWithBottomType then Nil else List(child) + else if (child.is(Private) || child.is(Sealed)) && child.isOneOf(AbstractOrTrait) then getChildren(child) + else List(child) } - val children = trace(i"getChildren($tp)")(getChildren(tp.classSymbol, typeArgs)) + val children = trace(i"getChildren($tp)")(getChildren(tp.classSymbol)) val parts = children.map { sym => val sym1 = if (sym.is(ModuleClass)) sym.sourceModule else sym val refined = trace(i"refineUsingParent($tp, $sym1, $mixins)")(TypeOps.refineUsingParent(tp, sym1, mixins)) + def containsUninhabitedField(tp: Type): Boolean = + !tp.typeSymbol.is(ModuleClass) && tp.fields.exists { field => + !field.symbol.flags.is(Lazy) && field.info.dealias.isBottomType + } + def inhabited(tp: Type): Boolean = tp.dealias match case AndType(tp1, tp2) => !TypeComparer.provablyDisjoint(tp1, tp2) case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2) case tp: RefinedType => inhabited(tp.parent) - case tp: TypeRef => inhabited(tp.prefix) - case _ => true + case tp: TypeRef => !containsUninhabitedField(tp) && inhabited(tp.prefix) + case _ => !containsUninhabitedField(tp) if inhabited(refined) then refined else NoType diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 1e6fbd7bed17..38d5bcf3fe9b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -124,8 +124,11 @@ object Applications { } def productSelectorTypes(tp: Type, errorPos: SrcPos)(using Context): List[Type] = { - val sels = for (n <- Iterator.from(0)) yield extractorMemberType(tp, nme.selectorName(n), errorPos) - sels.takeWhile(_.exists).toList + if tp.isError then + Nil + else + val sels = for (n <- Iterator.from(0)) yield extractorMemberType(tp, nme.selectorName(n), errorPos) + sels.takeWhile(_.exists).toList } def tupleComponentTypes(tp: Type)(using Context): List[Type] = @@ -479,7 +482,7 @@ trait Applications extends Compatibility { case tp => args.size } - !isAnnotConstr(methRef.symbol) && + !isJavaAnnotConstr(methRef.symbol) && args.size < requiredArgNum(funType) } @@ -611,11 +614,6 @@ trait Applications extends Compatibility { def isJavaAnnotConstr(sym: Symbol): Boolean = sym.is(JavaDefined) && sym.isConstructor && sym.owner.is(JavaAnnotation) - - /** Is `sym` a constructor of an annotation? */ - def isAnnotConstr(sym: Symbol): Boolean = - sym.isConstructor && sym.owner.isAnnotation - /** Match re-ordered arguments against formal parameters * @param n The position of the first parameter in formals in `methType`. */ @@ -676,6 +674,11 @@ trait Applications extends Compatibility { def implicitArg = implicitArgTree(formal, appPos.span) if !defaultArg.isEmpty then + if methodType.isImplicitMethod && ctx.mode.is(Mode.ImplicitsEnabled) + && !inferImplicitArg(formal, appPos.span).tpe.isError + then + report.warning(DefaultShadowsGiven(methodType.paramNames(n)), appPos) + defaultArg.tpe.widen match case _: MethodOrPoly if testOnly => matchArgs(args1, formals1, n + 1) case _ => matchArgs(args1, addTyped(treeToArg(defaultArg)), n + 1) @@ -843,7 +846,7 @@ trait Applications extends Compatibility { def makeVarArg(n: Int, elemFormal: Type): Unit = { val args = typedArgBuf.takeRight(n).toList typedArgBuf.dropRightInPlace(n) - val elemtpt = TypeTree(elemFormal) + val elemtpt = TypeTree(elemFormal.normalizedTupleType) typedArgBuf += seqToRepeated(SeqLiteral(args, elemtpt)) } @@ -905,8 +908,6 @@ trait Applications extends Compatibility { val app1 = if !success then app0.withType(UnspecifiedErrorType) else { - if isAnnotConstr(methRef.symbol) && !isJavaAnnotConstr(methRef.symbol) then - typedArgs if !sameSeq(args, orderedArgs) && !isJavaAnnotConstr(methRef.symbol) && !typedArgs.forall(isSafeArg) @@ -1111,9 +1112,8 @@ trait Applications extends Compatibility { // // summonFrom { // case given A[t] => - // summonFrom + // summonFrom: // case given `t` => ... - // } // } // // the second `summonFrom` should expand only once the first `summonFrom` is diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index bf99def353b0..8b6e875ab6ff 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -274,11 +274,16 @@ object Checking { */ def checkInfo(tp: Type): Type = tp match { case tp @ TypeAlias(alias) => - tp.derivedAlias(checkPart(alias, "alias")) + val lo1 = atVariance(-1)(checkPart(alias, "alias")) + val hi1 = checkUpper(alias, "alias") + if lo1 eq hi1 then + tp.derivedAlias(lo1) + else + tp.derivedTypeBounds(lo1, hi1) case tp @ MatchAlias(alias) => - tp.derivedAlias(checkUpper(alias, "match")) + tp.derivedAlias(atVariance(0)(checkUpper(alias, "match"))) case tp @ TypeBounds(lo, hi) => - tp.derivedTypeBounds(checkPart(lo, "lower bound"), checkUpper(hi, "upper bound")) + tp.derivedTypeBounds(atVariance(-1)(checkPart(lo, "lower bound")), checkUpper(hi, "upper bound")) case _ => tp } @@ -299,12 +304,12 @@ object Checking { case tp: TermRef => this(tp.info) mapOver(tp) - case tp @ AppliedType(tycon, args) => - tp.derivedAppliedType(this(tycon), args.mapConserve(this(_, nestedCycleOK, nestedCycleOK))) case tp @ RefinedType(parent, name, rinfo) => tp.derivedRefinedType(this(parent), name, this(rinfo, nestedCycleOK, nestedCycleOK)) case tp: RecType => tp.rebind(this(tp.parent)) + case tp: LazyRef => + tp case tp @ TypeRef(pre, _) => try { // A prefix is interesting if it might contain (transitively) a reference @@ -337,14 +342,17 @@ object Checking { if isInteresting(pre) then CyclicReference.trace(i"explore ${tp.symbol} for cyclic references"): - val pre1 = this(pre, false, false) + val pre1 = atVariance(variance max 0)(this(pre, false, false)) if locked.contains(tp) || tp.symbol.infoOrCompleter.isInstanceOf[NoCompleter] + && tp.symbol == sym then throw CyclicReference(tp.symbol) locked += tp try - if tp.symbol.isOpaqueAlias then + if tp.symbol.infoOrCompleter.isInstanceOf[NoCompleter] then + ; // skip checking info (and avoid forcing the symbol with .isOpaqueAlias/etc) + else if tp.symbol.isOpaqueAlias then checkInfo(TypeAlias(tp.translucentSuperType)) else if !tp.symbol.isClass then checkInfo(tp.info) @@ -362,6 +370,16 @@ object Checking { } case _ => mapOver(tp) } + + override def mapArg(arg: Type, tparam: ParamInfo): Type = + val varianceDiff = variance != tparam.paramVarianceSign + atVariance(variance * tparam.paramVarianceSign): + // Using tests/pos/i22257.scala as an example, + // if we consider FP's lower-bound of Fixed[Node] + // than `Node` is a type argument in contravariant + // position, while the type parameter is covariant. + val nestedCycleOK1 = nestedCycleOK || variance != 0 && varianceDiff + this(arg, nestedCycleOK, nestedCycleOK1) } /** Under -Yrequire-targetName, if `sym` has an operator name, check that it has a @@ -702,7 +720,7 @@ object Checking { } /** Verify classes extending AnyVal meet the requirements */ - def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = { + def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = { def checkValueClassMember(stat: Tree) = stat match { case _: TypeDef if stat.symbol.isClass => report.error(ValueClassesMayNotDefineInner(clazz, stat.symbol), stat.srcPos) @@ -715,6 +733,14 @@ object Checking { case _ => report.error(ValueClassesMayNotContainInitalization(clazz), stat.srcPos) } + inline def checkParentIsNotAnyValAlias(): Unit = + cdef.rhs match { + case impl: Template => + val parent = impl.parents.head + if parent.symbol.isAliasType && parent.typeOpt.dealias =:= defn.AnyValType then + report.error(ValueClassCannotExtendAliasOfAnyVal(clazz, parent.symbol), cdef.srcPos) + case _ => () + } // We don't check synthesised enum anonymous classes that are generated from // enum extending a value class type (AnyVal or an alias of it) // The error message 'EnumMayNotBeValueClassesID' will take care of generating the error message (See #22236) @@ -729,6 +755,9 @@ object Checking { report.error(ValueClassesMayNotBeAbstract(clazz), clazz.srcPos) if (!clazz.isStatic) report.error(ValueClassesMayNotBeContainted(clazz), clazz.srcPos) + + checkParentIsNotAnyValAlias() + if (isDerivedValueClass(underlyingOfValueClass(clazz.asClass).classSymbol)) report.error(ValueClassesMayNotWrapAnotherValueClass(clazz), clazz.srcPos) else { @@ -737,6 +766,8 @@ object Checking { } clParamAccessors match { case param :: params => + if (defn.isContextFunctionType(param.info)) + report.error("value classes are not allowed for context function types", param.srcPos) if (param.is(Mutable)) report.error(ValueClassParameterMayNotBeAVar(clazz, param), param.srcPos) if (param.info.isInstanceOf[ExprType]) @@ -1208,8 +1239,8 @@ trait Checking { else tpt /** Verify classes extending AnyVal meet the requirements */ - def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = - Checking.checkDerivedValueClass(clazz, stats) + def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = + Checking.checkDerivedValueClass(cdef, clazz, stats) /** Check that case classes are not inherited by case classes. */ @@ -1580,7 +1611,7 @@ trait NoChecking extends ReChecking { override def checkNoTargetNameConflict(stats: List[Tree])(using Context): Unit = () override def checkParentCall(call: Tree, caller: ClassSymbol)(using Context): Unit = () override def checkSimpleKinded(tpt: Tree)(using Context): Tree = tpt - override def checkDerivedValueClass(clazz: Symbol, stats: List[Tree])(using Context): Unit = () + override def checkDerivedValueClass(cdef: untpd.TypeDef, clazz: Symbol, stats: List[Tree])(using Context): Unit = () override def checkCaseInheritance(parentSym: Symbol, caseCls: ClassSymbol, pos: SrcPos)(using Context): Unit = () override def checkNoForwardDependencies(vparams: List[ValDef])(using Context): Unit = () override def checkMembersOK(tp: Type, pos: SrcPos)(using Context): Type = tp diff --git a/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala b/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala index ed96e3cacd4b..2122c10ad625 100644 --- a/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/CrossVersionChecks.scala @@ -78,7 +78,8 @@ class CrossVersionChecks extends MiniPhase: do val msg = annot.argumentConstantString(0).map(msg => s": $msg").getOrElse("") val since = annot.argumentConstantString(1).map(version => s" (since: $version)").getOrElse("") - report.deprecationWarning(em"inheritance from $psym is deprecated$since$msg", parent.srcPos, origin=psym.showFullName) + val composed = em"inheritance from $psym is deprecated$since$msg" + report.deprecationWarning(composed, parent.srcPos, origin = psym.showFullName) } override def transformValDef(tree: ValDef)(using Context): ValDef = @@ -166,16 +167,19 @@ object CrossVersionChecks: * Also check for deprecation of the companion class for synthetic methods in the companion module. */ private[CrossVersionChecks] def checkDeprecatedRef(sym: Symbol, pos: SrcPos)(using Context): Unit = - def maybeWarn(annotee: Symbol, annot: Annotation) = if !skipWarning(sym) then + def warn(annotee: Symbol, annot: Annotation) = val message = annot.argumentConstantString(0).filter(!_.isEmpty).map(": " + _).getOrElse("") val since = annot.argumentConstantString(1).filter(!_.isEmpty).map(" since " + _).getOrElse("") - report.deprecationWarning(em"${annotee.showLocated} is deprecated${since}${message}", pos, origin=annotee.showFullName) + val composed = em"${annotee.showLocated} is deprecated${since}${message}" + report.deprecationWarning(composed, pos, origin = annotee.showFullName) sym.getAnnotation(defn.DeprecatedAnnot) match - case Some(annot) => maybeWarn(sym, annot) + case Some(annot) => if !skipWarning(sym) then warn(sym, annot) case _ => if sym.isAllOf(SyntheticMethod) then val companion = sym.owner.companionClass - if companion.is(CaseClass) then companion.getAnnotation(defn.DeprecatedAnnot).foreach(maybeWarn(companion, _)) + if companion.is(CaseClass) then + for annot <- companion.getAnnotation(defn.DeprecatedAnnot) if !skipWarning(sym) do + warn(companion, annot) /** Decide whether the deprecation of `sym` should be ignored in this context. * diff --git a/compiler/src/dotty/tools/dotc/typer/Deriving.scala b/compiler/src/dotty/tools/dotc/typer/Deriving.scala index cc940ee38e41..febb12cf1007 100644 --- a/compiler/src/dotty/tools/dotc/typer/Deriving.scala +++ b/compiler/src/dotty/tools/dotc/typer/Deriving.scala @@ -10,7 +10,7 @@ import Contexts.*, Symbols.*, Types.*, SymDenotations.*, Names.*, NameOps.*, Fla import ProtoTypes.*, ContextOps.* import util.Spans.* import util.SrcPos -import collection.mutable +import collection.mutable.ListBuffer import ErrorReporting.errorTree /** A typer mixin that implements type class derivation functionality */ @@ -25,8 +25,8 @@ trait Deriving { */ class Deriver(cls: ClassSymbol, codePos: SrcPos)(using Context) { - /** A buffer for synthesized symbols for type class instances */ - private var synthetics = new mutable.ListBuffer[Symbol] + /** A buffer for synthesized symbols for type class instances, with what user asked to synthesize. */ + private val synthetics = ListBuffer.empty[(tpd.Tree, Symbol)] /** A version of Type#underlyingClassRef that works also for higher-kinded types */ private def underlyingClassRef(tp: Type): Type = tp match { @@ -41,7 +41,7 @@ trait Deriving { * an instance with the same name does not exist already. * @param reportErrors Report an error if an instance with the same name exists already */ - private def addDerivedInstance(clsName: Name, info: Type, pos: SrcPos): Unit = { + private def addDerivedInstance(derived: tpd.Tree, clsName: Name, info: Type, pos: SrcPos): Unit = { val instanceName = "derived$".concat(clsName) if (ctx.denotNamed(instanceName).exists) report.error(em"duplicate type class derivation for $clsName", pos) @@ -50,9 +50,8 @@ trait Deriving { // derived instance will have too low a priority to be selected over a freshly // derived instance at the summoning site. val flags = if info.isInstanceOf[MethodOrPoly] then GivenMethod else Given | Lazy - synthetics += - newSymbol(ctx.owner, instanceName, flags, info, coord = pos.span) - .entered + val sym = newSymbol(ctx.owner, instanceName, flags, info, coord = pos.span).entered + synthetics += derived -> sym } /** Check derived type tree `derived` for the following well-formedness conditions: @@ -77,7 +76,8 @@ trait Deriving { * that have the same name but different prefixes through selective aliasing. */ private def processDerivedInstance(derived: untpd.Tree): Unit = { - val originalTypeClassType = typedAheadType(derived, AnyTypeConstructorProto).tpe + val originalTypeClassTree = typedAheadType(derived, AnyTypeConstructorProto) + val originalTypeClassType = originalTypeClassTree.tpe val underlyingClassType = underlyingClassRef(originalTypeClassType) val typeClassType = checkClassType( underlyingClassType.orElse(originalTypeClassType), @@ -100,7 +100,7 @@ trait Deriving { val derivedInfo = if derivedParams.isEmpty then monoInfo else PolyType.fromParams(derivedParams, monoInfo) - addDerivedInstance(originalTypeClassType.typeSymbol.name, derivedInfo, derived.srcPos) + addDerivedInstance(originalTypeClassTree, originalTypeClassType.typeSymbol.name, derivedInfo, derived.srcPos) } def deriveSingleParameter: Unit = { @@ -312,7 +312,7 @@ trait Deriving { else tpd.ValDef(sym.asTerm, typeclassInstance(sym)(Nil)) } - synthetics.map(syntheticDef).toList + synthetics.map((t, s) => syntheticDef(s).withAttachment(Deriving.OriginalTypeClass, t)).toList } def finalize(stat: tpd.TypeDef): tpd.Tree = { @@ -321,3 +321,8 @@ trait Deriving { } } } +object Deriving: + import dotty.tools.dotc.util.Property + + /** Attachment holding the name of a type class as written by the user. */ + val OriginalTypeClass = Property.StickyKey[tpd.Tree] diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 69237c2aa493..50a7686c3db3 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -1250,11 +1250,15 @@ trait Implicits: val history = ctx.searchHistory.nest(cand, pt) val typingCtx = nestedContext().setNewTyperState().setFreshGADTBounds.setSearchHistory(history) + val alreadyStoppedInlining = ctx.base.stopInlining val result = typedImplicit(cand, pt, argument, span)(using typingCtx) result match case res: SearchSuccess => ctx.searchHistory.defineBynameImplicit(wideProto, res) case _ => + if !alreadyStoppedInlining && ctx.base.stopInlining then + // a call overflowed as part of the expansion when typing the implicit + ctx.base.stopInlining = false // Since the search failed, the local typerstate will be discarded // without being committed, but type variables local to that state // might still appear in an error message, so we run `gc()` here to diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 1c30f44c29cf..20a48edba757 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -68,7 +68,7 @@ trait ImportSuggestions: && !(root.name == nme.raw.BAR && ctx.settings.scalajs.value && root == JSDefinitions.jsdefn.PseudoUnionModule) } - def nestedRoots(site: Type)(using Context): List[Symbol] = + def nestedRoots(site: Type, parentSymbols: Set[Symbol])(using Context): List[Symbol] = val seenNames = mutable.Set[Name]() site.baseClasses.flatMap { bc => bc.info.decls.filter { dcl => @@ -78,34 +78,37 @@ trait ImportSuggestions: } } - def rootsStrictlyIn(ref: Type)(using Context): List[TermRef] = + def rootsStrictlyIn(ref: Type, parentSymbols: Set[Symbol] = Set())(using Context): List[TermRef] = val site = ref.widen val refSym = site.typeSymbol - val nested = - if refSym.is(Package) then - if refSym == defn.EmptyPackageClass // Don't search the empty package - || refSym == defn.JavaPackageClass // As an optimization, don't search java... - || refSym == defn.JavaLangPackageClass // ... or java.lang. - then Nil - else refSym.info.decls.filter(lookInside) - else if refSym.infoOrCompleter.isInstanceOf[StubInfo] then - Nil // Don't chase roots that do not exist - else - if !refSym.is(Touched) then - refSym.ensureCompleted() // JavaDefined is reliably known only after completion - if refSym.is(JavaDefined) then Nil - else nestedRoots(site) - nested - .map(mbr => TermRef(ref, mbr.asTerm)) - .flatMap(rootsIn) - .toList + if parentSymbols.contains(refSym) then Nil + else + val nested = + if refSym.is(Package) then + if refSym == defn.EmptyPackageClass // Don't search the empty package + || refSym == defn.JavaPackageClass // As an optimization, don't search java... + || refSym == defn.JavaLangPackageClass // ... or java.lang. + then Nil + else refSym.info.decls.filter(lookInside) + else if refSym.infoOrCompleter.isInstanceOf[StubInfo] then + Nil // Don't chase roots that do not exist + else + if !refSym.is(Touched) then + refSym.ensureCompleted() // JavaDefined is reliably known only after completion + if refSym.is(JavaDefined) then Nil + else nestedRoots(site, parentSymbols) + val newParentSymbols = parentSymbols + refSym + nested + .map(mbr => TermRef(ref, mbr.asTerm)) + .flatMap(rootsIn(_, newParentSymbols)) + .toList - def rootsIn(ref: TermRef)(using Context): List[TermRef] = + def rootsIn(ref: TermRef, parentSymbols: Set[Symbol] = Set())(using Context): List[TermRef] = if seen.contains(ref) then Nil else implicitsDetailed.println(i"search for suggestions in ${ref.symbol.fullName}") seen += ref - ref :: rootsStrictlyIn(ref) + ref :: rootsStrictlyIn(ref, parentSymbols) def rootsOnPath(tp: Type)(using Context): List[TermRef] = tp match case ref: TermRef => rootsIn(ref) ::: rootsOnPath(ref.prefix) diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 7acc704fd00d..2da75a0748b5 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -377,7 +377,9 @@ object Inferencing { } /** The instantiation decision for given poly param computed from the constraint. */ - enum Decision { case Min; case Max; case ToMax; case Skip; case Fail } + enum Decision: + case Min, Max, ToMax, Skip, Fail + private def instDecision(tvar: TypeVar, v: Int, minimizeSelected: Boolean, ifBottom: IfBottom)(using Context): Decision = import Decision.* val direction = instDirection(tvar.origin) @@ -442,10 +444,15 @@ object Inferencing { } } } - val res = patternBindings.toList.map { (boundSym, _) => + val res = patternBindings.toList.map { (boundSym, origin) => // substitute bounds of pattern bound variables to deal with possible F-bounds for (wildCard, param) <- patternBindings do boundSym.info = boundSym.info.substParam(param, wildCard.typeRef) + + // also substitute in any GADT bounds + // e.g. in i22879, replace the `T` in `X <: Iterable[T]` with the pattern bound `T$1` + ctx.gadtState.replace(origin, boundSym.typeRef) + boundSym } @@ -604,7 +611,6 @@ trait Inferencing { this: Typer => // This is needed because it could establish singleton type upper bounds. See i2998.scala. val tp = tree.tpe.widen - val vs = variances(tp, pt) // Avoid interpolating variables occurring in tree's type if typerstate has unreported errors. // Reason: The errors might reflect unsatisfiable constraints. In that @@ -628,135 +634,138 @@ trait Inferencing { this: Typer => // val y: List[List[String]] = List(List(1)) if state.reporter.hasUnreportedErrors then return tree - def constraint = state.constraint - - trace(i"interpolateTypeVars($tree: ${tree.tpe}, $pt, $qualifying)", typr, (_: Any) => i"$qualifying\n$constraint\n${ctx.gadt}") { - //println(i"$constraint") - //println(i"${ctx.gadt}") - - /** Values of this type report type variables to instantiate with variance indication: - * +1 variable appears covariantly, can be instantiated from lower bound - * -1 variable appears contravariantly, can be instantiated from upper bound - * 0 variable does not appear at all, can be instantiated from either bound - */ - type ToInstantiate = List[(TypeVar, Int)] - - val toInstantiate: ToInstantiate = - val buf = new mutable.ListBuffer[(TypeVar, Int)] - for tvar <- qualifying do - if !tvar.isInstantiated && constraint.contains(tvar) && tvar.nestingLevel >= ctx.nestingLevel then - constrainIfDependentParamRef(tvar, tree) - if !tvar.isInstantiated then - // isInstantiated needs to be checked again, since previous interpolations could already have - // instantiated `tvar` through unification. - val v = vs.computedVariance(tvar) - if v == null then buf += ((tvar, 0)) - else if v.intValue != 0 then buf += ((tvar, v.intValue)) - else comparing(cmp => - if !cmp.levelOK(tvar.nestingLevel, ctx.nestingLevel) then - // Invariant: The type of a tree whose enclosing scope is level - // N only contains type variables of level <= N. - typr.println(i"instantiate nonvariant $tvar of level ${tvar.nestingLevel} to a type variable of level <= ${ctx.nestingLevel}, $constraint") - cmp.atLevel(ctx.nestingLevel, tvar.origin) - else - typr.println(i"no interpolation for nonvariant $tvar in $state") - ) - // constrainIfDependentParamRef could also have instantiated tvars added to buf before the check - buf.filterNot(_._1.isInstantiated).toList - end toInstantiate - - def typeVarsIn(xs: ToInstantiate): TypeVars = - xs.foldLeft(SimpleIdentitySet.empty: TypeVars)((tvs, tvi) => tvs + tvi._1) - - /** Filter list of proposed instantiations so that they don't constrain further - * the current constraint. - */ - def filterByDeps(tvs0: ToInstantiate): ToInstantiate = - val excluded = // ignore dependencies from other variables that are being instantiated - typeVarsIn(tvs0) - def step(tvs: ToInstantiate): ToInstantiate = tvs match - case tvs @ (hd @ (tvar, v)) :: tvs1 => - def aboveOK = !constraint.dependsOn(tvar, excluded, co = true) - def belowOK = !constraint.dependsOn(tvar, excluded, co = false) - if v == 0 && !aboveOK then - step((tvar, 1) :: tvs1) - else if v == 0 && !belowOK then - step((tvar, -1) :: tvs1) - else if v == -1 && !aboveOK || v == 1 && !belowOK then - typr.println(i"drop $tvar, $v in $tp, $pt, qualifying = ${qualifying.toList}, tvs0 = ${tvs0.toList}%, %, excluded = ${excluded.toList}, $constraint") - step(tvs1) - else // no conflict, keep the instantiation proposal - tvs.derivedCons(hd, step(tvs1)) - case Nil => - Nil - val tvs1 = step(tvs0) - if tvs1 eq tvs0 then tvs1 - else filterByDeps(tvs1) // filter again with smaller excluded set - end filterByDeps - - /** Instantiate all type variables in `tvs` in the indicated directions, - * as described in the doc comment of `ToInstantiate`. - * If a type variable A is instantiated from below, and there is another - * type variable B in `buf` that is known to be smaller than A, wait and - * instantiate all other type variables before trying to instantiate A again. - * Dually, wait instantiating a type variable from above as long as it has - * upper bounds in `buf`. - * - * This is done to avoid loss of precision when forming unions. An example - * is in i7558.scala: - * - * type Tr[+V1, +O1 <: V1] - * extension [V2, O2 <: V2](tr: Tr[V2, O2]) def sl: Tr[V2, O2] = ??? - * def as[V3, O3 <: V3](tr: Tr[V3, O3]) : Tr[V3, O3] = tr.sl - * - * Here we interpolate at some point V2 and O2 given the constraint - * - * V2 >: V3, O2 >: O3, O2 <: V2 - * - * where O3 and V3 are type refs with O3 <: V3. - * If we interpolate V2 first to V3 | O2, the widenUnion algorithm will - * instantiate O2 to V3, leading to the final constraint - * - * V2 := V3, O2 := V3 - * - * But if we instantiate O2 first to O3, and V2 next to V3, we get the - * more flexible instantiation - * - * V2 := V3, O2 := O3 - */ - def doInstantiate(tvs: ToInstantiate): Unit = - - /** Try to instantiate `tvs`, return any suspended type variables */ - def tryInstantiate(tvs: ToInstantiate): ToInstantiate = tvs match - case (hd @ (tvar, v)) :: tvs1 => - val fromBelow = v == 1 || (v == 0 && tvar.hasLowerBound) - typr.println( - i"interpolate${if v == 0 then " non-occurring" else ""} $tvar in $state in $tree: $tp, fromBelow = $fromBelow, $constraint") - if tvar.isInstantiated then - tryInstantiate(tvs1) - else - val suspend = tvs1.exists{ (following, _) => - if fromBelow - then constraint.isLess(following.origin, tvar.origin) - else constraint.isLess(tvar.origin, following.origin) - } - if suspend then - typr.println(i"suspended: $hd") - hd :: tryInstantiate(tvs1) - else - tvar.instantiate(fromBelow) - tryInstantiate(tvs1) - case Nil => Nil - if tvs.nonEmpty then doInstantiate(tryInstantiate(tvs)) - end doInstantiate - - doInstantiate(filterByDeps(toInstantiate)) - } + instantiateTypeVars(tp, pt, qualifying, tree) } end if tree end interpolateTypeVars + def instantiateTypeVars(tp: Type, pt: Type, qualifying: List[TypeVar], tree: Tree = EmptyTree)(using Context): Unit = + trace(i"instantiateTypeVars($tp, $pt, $qualifying, $tree)", typr): + val state = ctx.typerState + def constraint = state.constraint + + val vs = variances(tp, pt) + + /** Values of this type report type variables to instantiate with variance indication: + * +1 variable appears covariantly, can be instantiated from lower bound + * -1 variable appears contravariantly, can be instantiated from upper bound + * 0 variable does not appear at all, can be instantiated from either bound + */ + type ToInstantiate = List[(TypeVar, Int)] + + val toInstantiate: ToInstantiate = + val buf = new mutable.ListBuffer[(TypeVar, Int)] + for tvar <- qualifying do + if !tvar.isInstantiated && constraint.contains(tvar) && tvar.nestingLevel >= ctx.nestingLevel then + constrainIfDependentParamRef(tvar, tree) + if !tvar.isInstantiated then + // isInstantiated needs to be checked again, since previous interpolations could already have + // instantiated `tvar` through unification. + val v = vs.computedVariance(tvar) + if v == null then buf += ((tvar, 0)) + else if v.intValue != 0 then buf += ((tvar, v.intValue)) + else comparing(cmp => + if !cmp.levelOK(tvar.nestingLevel, ctx.nestingLevel) then + // Invariant: The type of a tree whose enclosing scope is level + // N only contains type variables of level <= N. + typr.println(i"instantiate nonvariant $tvar of level ${tvar.nestingLevel} to a type variable of level <= ${ctx.nestingLevel}, $constraint") + cmp.atLevel(ctx.nestingLevel, tvar.origin) + else + typr.println(i"no interpolation for nonvariant $tvar in $state") + ) + // constrainIfDependentParamRef could also have instantiated tvars added to buf before the check + buf.filterNot(_._1.isInstantiated).toList + end toInstantiate + + def typeVarsIn(xs: ToInstantiate): TypeVars = + xs.foldLeft(SimpleIdentitySet.empty: TypeVars)((tvs, tvi) => tvs + tvi._1) + + /** Filter list of proposed instantiations so that they don't constrain further + * the current constraint. + */ + def filterByDeps(tvs0: ToInstantiate): ToInstantiate = + val excluded = // ignore dependencies from other variables that are being instantiated + typeVarsIn(tvs0) + def step(tvs: ToInstantiate): ToInstantiate = tvs match + case tvs @ (hd @ (tvar, v)) :: tvs1 => + def aboveOK = !constraint.dependsOn(tvar, excluded, co = true) + def belowOK = !constraint.dependsOn(tvar, excluded, co = false) + if v == 0 && !aboveOK then + step((tvar, 1) :: tvs1) + else if v == 0 && !belowOK then + step((tvar, -1) :: tvs1) + else if v == -1 && !aboveOK || v == 1 && !belowOK then + typr.println(i"drop $tvar, $v in $tp, $pt, qualifying = ${qualifying.toList}, tvs0 = ${tvs0.toList}%, %, excluded = ${excluded.toList}, $constraint") + step(tvs1) + else // no conflict, keep the instantiation proposal + tvs.derivedCons(hd, step(tvs1)) + case Nil => + Nil + val tvs1 = step(tvs0) + if tvs1 eq tvs0 then tvs1 + else filterByDeps(tvs1) // filter again with smaller excluded set + end filterByDeps + + /** Instantiate all type variables in `tvs` in the indicated directions, + * as described in the doc comment of `ToInstantiate`. + * If a type variable A is instantiated from below, and there is another + * type variable B in `buf` that is known to be smaller than A, wait and + * instantiate all other type variables before trying to instantiate A again. + * Dually, wait instantiating a type variable from above as long as it has + * upper bounds in `buf`. + * + * This is done to avoid loss of precision when forming unions. An example + * is in i7558.scala: + * + * type Tr[+V1, +O1 <: V1] + * extension [V2, O2 <: V2](tr: Tr[V2, O2]) def sl: Tr[V2, O2] = ??? + * def as[V3, O3 <: V3](tr: Tr[V3, O3]) : Tr[V3, O3] = tr.sl + * + * Here we interpolate at some point V2 and O2 given the constraint + * + * V2 >: V3, O2 >: O3, O2 <: V2 + * + * where O3 and V3 are type refs with O3 <: V3. + * If we interpolate V2 first to V3 | O2, the widenUnion algorithm will + * instantiate O2 to V3, leading to the final constraint + * + * V2 := V3, O2 := V3 + * + * But if we instantiate O2 first to O3, and V2 next to V3, we get the + * more flexible instantiation + * + * V2 := V3, O2 := O3 + */ + def doInstantiate(tvs: ToInstantiate): Unit = + + /** Try to instantiate `tvs`, return any suspended type variables */ + def tryInstantiate(tvs: ToInstantiate): ToInstantiate = tvs match + case (hd @ (tvar, v)) :: tvs1 => + val fromBelow = v == 1 || (v == 0 && tvar.hasLowerBound) + typr.println( + i"interpolate${if v == 0 then " non-occurring" else ""} $tvar in $state in $tree: $tp, fromBelow = $fromBelow, $constraint") + if tvar.isInstantiated then + tryInstantiate(tvs1) + else + val suspend = tvs1.exists{ (following, _) => + if fromBelow + then constraint.isLess(following.origin, tvar.origin) + else constraint.isLess(tvar.origin, following.origin) + } + if suspend then + typr.println(i"suspended: $hd") + hd :: tryInstantiate(tvs1) + else + tvar.instantiate(fromBelow) + tryInstantiate(tvs1) + case Nil => Nil + if tvs.nonEmpty then doInstantiate(tryInstantiate(tvs)) + end doInstantiate + + doInstantiate(filterByDeps(toInstantiate)) + end instantiateTypeVars + /** If `tvar` represents a parameter of a dependent method type in the current `call` * approximate it from below with the type of the actual argument. Skolemize that * type if necessary to make it a Singleton. diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 0471bb6a432a..2269a07ec77c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -131,7 +131,7 @@ class Namer { typer: Typer => * The logic here is very subtle and fragile due to the fact that * we are not allowed to force anything. */ - def checkNoConflict(name: Name, isPrivate: Boolean, span: Span)(using Context): Name = + def checkNoConflict(name: Name, span: Span)(using Context): Name = val owner = ctx.owner var conflictsDetected = false @@ -166,7 +166,7 @@ class Namer { typer: Typer => def preExisting = ctx.effectiveScope.lookup(name) if (!owner.isClass || name.isTypeName) && preExisting.exists then conflict(preExisting) - else if owner.isPackageObject && !isPrivate && name != nme.CONSTRUCTOR then + else if owner.isPackageObject && name != nme.CONSTRUCTOR then checkNoConflictIn(owner.owner) for pkgObj <- pkgObjs(owner.owner) if pkgObj != owner do checkNoConflictIn(pkgObj) @@ -242,7 +242,7 @@ class Namer { typer: Typer => tree match { case tree: TypeDef if tree.isClassDef => val flags = checkFlags(tree.mods.flags) - val name = checkNoConflict(tree.name, flags.is(Private), tree.span).asTypeName + val name = checkNoConflict(tree.name, tree.span).asTypeName val cls = createOrRefine[ClassSymbol](tree, name, flags, ctx.owner, cls => adjustIfModule(new ClassCompleter(cls, tree)(ctx), tree), @@ -251,7 +251,7 @@ class Namer { typer: Typer => cls case tree: MemberDef => var flags = checkFlags(tree.mods.flags) - val name = checkNoConflict(tree.name, flags.is(Private), tree.span) + val name = checkNoConflict(tree.name, tree.span) tree match case tree: ValOrDefDef => if tree.isInstanceOf[ValDef] && !flags.is(Param) && name.endsWith("_=") then @@ -694,7 +694,13 @@ class Namer { typer: Typer => enterSymbol(classConstructorCompanion(classSym.asClass)) else for moduleSym <- companionVals do - if moduleSym.is(Module) && !moduleSym.isDefinedInCurrentRun then + // by not going through `.lastKnownDenotation` (instead using `.current`), + // we guarantee that the `moduleSym` will be brought forward to the current run, + // rendering `moduleSym.isDefinedInCurrentRun` as always true. + // We want to regenerate the companion instead of bringing it forward, + // as even if we are able to bring forward the object symbol, + // we might not be able to do the same with its stale module class symbol (see `tests/pos/i20449`) + if moduleSym.lastKnownDenotation.is(Module) && !moduleSym.isDefinedInCurrentRun then val companion = if needsConstructorProxies(classSym) then classConstructorCompanion(classSym.asClass) @@ -1083,7 +1089,8 @@ class Namer { typer: Typer => end typeSig } - class ClassCompleter(cls: ClassSymbol, original: TypeDef)(ictx: Context) extends Completer(original)(ictx) { + class ClassCompleter(cls: ClassSymbol, original: TypeDef)(ictx: Context) + extends Completer(original)(ictx), CompleterWithCleanup { withDecls(newScope(using ictx)) protected implicit val completerCtx: Context = localContext(cls) @@ -1130,11 +1137,26 @@ class Namer { typer: Typer => def canForward(mbr: SingleDenotation, alias: TermName): CanForward = { import CanForward.* val sym = mbr.symbol + /** + * The export selects a member of the current class (issue #22147). + * Assumes that cls.classInfo.selfType.derivesFrom(sym.owner) is true. + */ + def isCurrentClassMember: Boolean = expr match + case id: (Ident | This) => // Access through self type or this + /* Given the usage context below, where cls's self type is a subtype of sym.owner, + it suffices to check if symbol is the same class. */ + cls == id.symbol + case _ => false if !sym.isAccessibleFrom(pathType) then No("is not accessible") else if sym.isConstructor || sym.is(ModuleClass) || sym.is(Bridge) || sym.is(ConstructorProxy) || sym.isAllOf(JavaModule) then Skip - else if cls.derivesFrom(sym.owner) && (sym.owner == cls || !sym.is(Deferred)) then + // if the cls is a subclass or mixes in the owner of the symbol + // and either + // * the symbols owner is the cls itself + // * the symbol is not a deferred symbol + // * the symbol is a member of the current class (#22147) + else if cls.classInfo.selfType.derivesFrom(sym.owner) && (sym.owner == cls || !sym.is(Deferred) || isCurrentClassMember) then No(i"is already a member of $cls") else if pathMethod.exists && mbr.isType then No("is a type, so it cannot be exported as extension method") @@ -1155,20 +1177,21 @@ class Namer { typer: Typer => Yes } - def foreachDefaultGetterOf(sym: TermSymbol, op: TermSymbol => Unit): Unit = + def foreachDefaultGetterOf(sym: TermSymbol, alias: TermName)(op: (TermSymbol, TermName) => Unit): Unit = var n = 0 - val methodName = - if sym.name == nme.apply && sym.is(Synthetic) && sym.owner.companionClass.is(Case) then - // The synthesized `apply` methods of case classes use the constructor's default getters - nme.CONSTRUCTOR - else sym.name + // The synthesized `apply` methods of case classes use the constructor's default getters + val useConstructor = sym.name == nme.apply && sym.is(Synthetic) && sym.owner.companionClass.is(Case) + val methodName = if useConstructor then nme.CONSTRUCTOR else sym.name + val aliasedName = if useConstructor then nme.CONSTRUCTOR else alias + val useAliased = !useConstructor && methodName != aliasedName for params <- sym.paramSymss; param <- params do if param.isTerm then if param.is(HasDefault) then val getterName = DefaultGetterName(methodName, n) val getter = pathType.member(getterName).symbol assert(getter.exists, i"$path does not have a default getter named $getterName") - op(getter.asTerm) + val targetName = if useAliased then DefaultGetterName(aliasedName, n) else getterName + op(getter.asTerm, targetName) n += 1 /** Add a forwarder with name `alias` or its type name equivalent to `mbr`, @@ -1196,7 +1219,7 @@ class Namer { typer: Typer => val hasDefaults = sym.hasDefaultParams // compute here to ensure HasDefaultParams and NoDefaultParams flags are set val forwarder = if mbr.isType then - val forwarderName = checkNoConflict(alias.toTypeName, isPrivate = false, span) + val forwarderName = checkNoConflict(alias.toTypeName, span) var target = pathType.select(sym) if target.typeParams.nonEmpty then target = target.EtaExpand(target.typeParams) @@ -1250,7 +1273,7 @@ class Namer { typer: Typer => (EmptyFlags, mbrInfo) var mbrFlags = MandatoryExportTermFlags | maybeStable | (sym.flags & RetainedExportTermFlags) if pathMethod.exists then mbrFlags |= ExtensionMethod - val forwarderName = checkNoConflict(alias, isPrivate = false, span) + val forwarderName = checkNoConflict(alias, span) newSymbol(cls, forwarderName, mbrFlags, mbrInfo, coord = span) forwarder.info = avoidPrivateLeaks(forwarder) @@ -1274,7 +1297,7 @@ class Namer { typer: Typer => val ddef = tpd.DefDef(forwarder.asTerm, prefss => { val forwarderCtx = ctx.withOwner(forwarder) val (pathRefss, methRefss) = prefss.splitAt(extensionParamsCount(path.tpe.widen)) - val ref = path.appliedToArgss(pathRefss).select(sym.asTerm) + val ref = path.appliedToArgss(pathRefss).select(sym.asTerm).withSpan(span.focus) val rhs = ref.appliedToArgss(adaptForwarderParams(Nil, sym.info, methRefss)) .etaExpandCFT(using forwarderCtx) if forwarder.isInlineMethod then @@ -1289,9 +1312,8 @@ class Namer { typer: Typer => }) buf += ddef.withSpan(span) if hasDefaults then - foreachDefaultGetterOf(sym.asTerm, - getter => addForwarder( - getter.name.asTermName, getter.asSeenFrom(path.tpe), span)) + foreachDefaultGetterOf(sym.asTerm, alias): (getter, getterName) => + addForwarder(getterName, getter.asSeenFrom(path.tpe), span) // adding annotations and flags at the parameter level // TODO: This probably needs to be filtered to avoid adding some annotation @@ -1346,13 +1368,13 @@ class Namer { typer: Typer => addWildcardForwardersNamed(alias, span) def addForwarders(sels: List[untpd.ImportSelector], seen: List[TermName]): Unit = sels match - case sel :: sels1 => + case sel :: sels => if sel.isWildcard then addWildcardForwarders(seen, sel.span) else if sel.rename != nme.WILDCARD then addForwardersNamed(sel.name, sel.rename, sel.span) - addForwarders(sels1, sel.name :: seen) + addForwarders(sels, sel.name :: seen) case _ => /** Avoid a clash of export forwarder `forwarder` with other forwarders in `forwarders`. @@ -1670,6 +1692,7 @@ class Namer { typer: Typer => processExports(using localCtx) defn.patchStdLibClass(cls) addConstructorProxies(cls) + cleanup() } } diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 9694822bc0d5..f8bbd6989a0f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -69,13 +69,6 @@ object ProtoTypes { |constraint was: ${ctx.typerState.constraint} |constraint now: ${newctx.typerState.constraint}""") if result && (ctx.typerState.constraint ne newctx.typerState.constraint) then - // Remove all type lambdas and tvars introduced by testCompat - for tvar <- newctx.typerState.ownedVars do - inContext(newctx): - if !tvar.isInstantiated then - tvar.instantiate(fromBelow = false) // any direction - - // commit any remaining changes in typer state newctx.typerState.commit() result case _ => testCompat @@ -398,21 +391,28 @@ object ProtoTypes { * - t2 is a ascription (t22: T) and t1 is at the outside of t22 * - t2 is a closure (...) => t22 and t1 is at the outside of t22 */ - def hasInnerErrors(t: Tree)(using Context): Boolean = t match - case Typed(expr, tpe) => hasInnerErrors(expr) - case closureDef(mdef) => hasInnerErrors(mdef.rhs) + def hasInnerErrors(t: Tree, argType: Type)(using Context): Boolean = t match + case Typed(expr, tpe) => hasInnerErrors(expr, argType) + case closureDef(mdef) => hasInnerErrors(mdef.rhs, argType) case _ => t.existsSubTree { t1 => if t1.typeOpt.isError && t.span.toSynthetic != t1.span.toSynthetic && t.typeOpt != t1.typeOpt then typr.println(i"error subtree $t1 of $t with ${t1.typeOpt}, spans = ${t1.span}, ${t.span}") - true + t1.typeOpt match + case errorType: ErrorType if errorType.msg.isInstanceOf[TypeMismatchMsg] => + // if error is caused by an argument type mismatch, + // then return false to try to find an extension. + // see i20335.scala for test case. + val typeMismtachMsg = errorType.msg.asInstanceOf[TypeMismatchMsg] + argType != typeMismtachMsg.expected + case _ => true else false } - private def cacheTypedArg(arg: untpd.Tree, typerFn: untpd.Tree => Tree, force: Boolean)(using Context): Tree = { + private def cacheTypedArg(arg: untpd.Tree, typerFn: untpd.Tree => Tree, force: Boolean, argType: Type)(using Context): Tree = { var targ = state.typedArg(arg) if (targ == null) untpd.functionWithUnknownParamType(arg) match { @@ -430,7 +430,7 @@ object ProtoTypes { targ = typerFn(arg) // TODO: investigate why flow typing is not working on `targ` if ctx.reporter.hasUnreportedErrors then - if hasInnerErrors(targ.nn) then + if hasInnerErrors(targ.nn, argType) then state.errorArgs += arg else state.typedArg = state.typedArg.updated(arg, targ.nn) @@ -458,7 +458,7 @@ object ProtoTypes { val protoTyperState = ctx.typerState val oldConstraint = protoTyperState.constraint val args1 = args.mapWithIndexConserve((arg, idx) => - cacheTypedArg(arg, arg => typer.typed(norm(arg, idx)), force = false)) + cacheTypedArg(arg, arg => typer.typed(norm(arg, idx)), force = false, NoType)) val newConstraint = protoTyperState.constraint if !args1.exists(arg => isUndefined(arg.tpe)) then state.typedArgs = args1 @@ -505,7 +505,8 @@ object ProtoTypes { val locked = ctx.typerState.ownedVars val targ = cacheTypedArg(arg, typer.typedUnadapted(_, wideFormal, locked)(using argCtx), - force = true) + force = true, + wideFormal) val targ1 = typer.adapt(targ, wideFormal, locked) if wideFormal eq formal then targ1 else checkNoWildcardCaptureForCBN(targ1) @@ -957,6 +958,15 @@ object ProtoTypes { paramInfos = tl.paramInfos.mapConserve(wildApprox(_, theMap, seen, internal1).bounds), resType = wildApprox(tl.resType, theMap, seen, internal1) ) + case tp @ AnnotatedType(parent, _) => + // This case avoids approximating types in the annotation tree, which can + // cause the type assigner to fail. + // See #22893 and tests/pos/annot-default-arg-22874.scala. + val parentApprox = wildApprox(parent, theMap, seen, internal) + if tp.isRefining then + WildcardType(TypeBounds.upper(parentApprox)) + else + parentApprox case _ => (if (theMap != null && seen.eq(theMap.seen)) theMap else new WildApproxMap(seen, internal)) .mapOver(tp) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 87768e8a2026..7114f609a41e 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -630,10 +630,16 @@ object RefChecks { // to consolidate getters and setters. val grouped = missing.groupBy(_.underlyingSymbol.name) + def isDuplicateSetter(sym: Symbol): Boolean = + sym.isSetter && { + val field = sym.accessedFieldOrGetter + grouped.getOrElse(field.name, Nil).contains(field) + } + val missingMethods = grouped.toList flatMap { case (name, syms) => lastOverrides(syms) - .filterConserve(!_.isSetter) + .filterConserve(!isDuplicateSetter(_)) // Avoid reporting override error for both `x` and setter `x_=` .distinctBy(_.signature) // Avoid duplication for similar definitions (#19731) } @@ -1033,14 +1039,24 @@ object RefChecks { end checkUnaryMethods /** Check that an extension method is not hidden, i.e., that it is callable as an extension method. + * + * For example, it is not possible to define a type-safe extension `contains` for `Set`, + * since for any parameter type, the existing `contains` method will compile and would be used. * * An extension method is hidden if it does not offer a parameter that is not subsumed * by the corresponding parameter of the member with the same name (or of all alternatives of an overload). * - * This check is suppressed if this method is an override. + * This check is suppressed if the method is an override. (Because the type of the receiver + * may be narrower in the override.) * - * For example, it is not possible to define a type-safe extension `contains` for `Set`, - * since for any parameter type, the existing `contains` method will compile and would be used. + * If the extension method is nilary, it is always hidden by a member of the same name. + * (Either the member is nilary, or the reference is taken as the eta-expansion of the member.) + * + * This check is in lieu of a more expensive use-site check that an application failed to use an extension. + * That check would account for accessibility and opacity. As a limitation, this check considers + * only public members for which corresponding method parameters are either both opaque types or both not. + * It is intended to warn if the receiver type from a third-party library has been augmented with a member + * that nullifies an existing extension. * * If the member has a leading implicit parameter list, then the extension method must also have * a leading implicit parameter list. The reason is that if the implicit arguments are inferred, @@ -1051,46 +1067,48 @@ object RefChecks { * If the member does not have a leading implicit parameter list, then the argument cannot be explicitly * supplied with `using`, as typechecking would fail. But the extension method may have leading implicit * parameters, which are necessarily supplied implicitly in the application. The first non-implicit - * parameters of the extension method must be distinguishable from the member parameters, as described. - * - * If the extension method is nullary, it is always hidden by a member of the same name. - * (Either the member is nullary, or the reference is taken as the eta-expansion of the member.) - * - * This check is in lieu of a more expensive use-site check that an application failed to use an extension. - * That check would account for accessibility and opacity. As a limitation, this check considers - * only public members, a target receiver that is not an alias, and corresponding method parameters - * that are either both opaque types or both not. + * parameters of the extension method must be distinguishable from the member parameters, as described above. */ def checkExtensionMethods(sym: Symbol)(using Context): Unit = - if sym.is(Extension) && !sym.nextOverriddenSymbol.exists then + if sym.is(Extension) then extension (tp: Type) - def strippedResultType = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).resultType - def firstExplicitParamTypes = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true).firstParamTypes + def explicit = Applications.stripImplicit(tp.stripPoly, wildcardOnly = true) def hasImplicitParams = tp.stripPoly match { case mt: MethodType => mt.isImplicitMethod case _ => false } - val target = sym.info.firstExplicitParamTypes.head // required for extension method, the putative receiver - val methTp = sym.info.strippedResultType // skip leading implicits and the "receiver" parameter - def hidden = - target.nonPrivateMember(sym.name) - .filterWithPredicate: - member => - member.symbol.isPublic && { - val memberIsImplicit = member.info.hasImplicitParams - val paramTps = - if memberIsImplicit then methTp.stripPoly.firstParamTypes - else methTp.firstExplicitParamTypes - - paramTps.isEmpty || memberIsImplicit && !methTp.hasImplicitParams || { - val memberParamTps = member.info.stripPoly.firstParamTypes - !memberParamTps.isEmpty - && memberParamTps.lengthCompare(paramTps) == 0 - && memberParamTps.lazyZip(paramTps).forall: (m, x) => - m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias - && (x frozen_<:< m) - } - } - .exists - if !target.typeSymbol.denot.isAliasType && !target.typeSymbol.denot.isOpaqueAlias && hidden - then report.warning(ExtensionNullifiedByMember(sym, target.typeSymbol), sym.srcPos) + def isNilary = tp.stripPoly match { case mt: MethodType => false case _ => true } + val explicitInfo = sym.info.explicit // consider explicit value params + def memberHidesMethod(member: Denotation): Boolean = + val methTp = explicitInfo.resultType // skip leading implicits and the "receiver" parameter + if methTp.isNilary then + return true // extension without parens is always hidden by a member of same name + val memberIsImplicit = member.info.hasImplicitParams + inline def paramsCorrespond = + val paramTps = + if memberIsImplicit then methTp.stripPoly.firstParamTypes + else methTp.explicit.firstParamTypes + val memberParamTps = member.info.stripPoly.firstParamTypes + memberParamTps.corresponds(paramTps): (m, x) => + m.typeSymbol.denot.isOpaqueAlias == x.typeSymbol.denot.isOpaqueAlias && (x frozen_<:< m) + memberIsImplicit && !methTp.hasImplicitParams || paramsCorrespond + def targetOfHiddenExtension: Symbol = + val target = + val target0 = explicitInfo.firstParamTypes.head // required for extension method, the putative receiver + target0.dealiasKeepOpaques.typeSymbol.info + val member = target.nonPrivateMember(sym.name) + .filterWithPredicate: member => + member.symbol.isPublic && memberHidesMethod(member) + if member.exists then target.typeSymbol else NoSymbol + if sym.is(HasDefaultParams) then + val getterDenot = + val receiverName = explicitInfo.firstParamNames.head + val num = sym.info.paramNamess.flatten.indexWhere(_ == receiverName) + val getterName = DefaultGetterName(sym.name.toTermName, num = num) + sym.owner.info.member(getterName) + if getterDenot.exists + then report.warning(ExtensionHasDefault(sym), getterDenot.symbol.srcPos) + if !sym.nextOverriddenSymbol.exists then + val target = targetOfHiddenExtension + if target.exists then + report.warning(ExtensionNullifiedByMember(sym, target), sym.srcPos) end checkExtensionMethods /** Verify that references in the user-defined `@implicitNotFound` message are valid. diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index dca270f683c9..0a653c5e8ef8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -18,6 +18,7 @@ import annotation.{tailrec, constructorOnly} import ast.tpd import ast.tpd.* import Synthesizer.* +import TypeComparer.{fullLowerBound, fullUpperBound} /** Synthesize terms for special classes */ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): @@ -27,17 +28,43 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): private type SpecialHandlers = List[(ClassSymbol, SpecialHandler)] val synthesizedClassTag: SpecialHandler = (formal, span) => - def instArg(tp: Type): Type = tp.stripTypeVar match - // Special case to avoid instantiating `Int & S` to `Int & Nothing` in - // i16328.scala. The intersection comes from an earlier instantiation - // to an upper bound. - // The dual situation with unions is harder to trigger because lower - // bounds are usually widened during instantiation. + def instArg(tp: Type): Type = tp.dealias match case tp: AndOrType if tp.tp1 =:= tp.tp2 => + // Special case to avoid instantiating `Int & S` to `Int & Nothing` in + // i16328.scala. The intersection comes from an earlier instantiation + // to an upper bound. + // The dual situation with unions is harder to trigger because lower + // bounds are usually widened during instantiation. instArg(tp.tp1) + case tvar: TypeVar if ctx.typerState.constraint.contains(tvar) => + // If tvar has a lower or upper bound: + // 1. If the bound is not another type variable, use this as approximation. + // 2. Otherwise, if the type can be forced to be fully defined, use that type + // as approximation. + // 3. Otherwise leave argument uninstantiated. + // The reason for (2) is that we observed complicated constraints in i23611.scala + // that get better types if a fully defined type is computed than if several type + // variables are approximated incrementally. This is a minimization of some ZIO code. + // So in order to keep backwards compatibility (where before we _only_ did 2) we + // add that special case. + def isGroundConstr(tp: Type): Boolean = tp.dealias match + case tvar: TypeVar if ctx.typerState.constraint.contains(tvar) => false + case pref: TypeParamRef if ctx.typerState.constraint.contains(pref) => false + case tp: AndOrType => isGroundConstr(tp.tp1) && isGroundConstr(tp.tp2) + case _ => true + instArg( + if tvar.hasLowerBound then + if isGroundConstr(fullLowerBound(tvar.origin)) then tvar.instantiate(fromBelow = true) + else if isFullyDefined(tp, ForceDegree.all) then tp + else NoType + else if tvar.hasUpperBound then + if isGroundConstr(fullUpperBound(tvar.origin)) then tvar.instantiate(fromBelow = false) + else if isFullyDefined(tp, ForceDegree.all) then tp + else NoType + else + NoType) case _ => - if isFullyDefined(tp, ForceDegree.all) then tp - else NoType // this happens in tests/neg/i15372.scala + tp val tag = formal.argInfos match case arg :: Nil => @@ -51,7 +78,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): if defn.SpecialClassTagClasses.contains(sym) then classTagModul.select(sym.name.toTermName).withSpan(span) else - val ctype = escapeJavaArray(erasure(tp)) + val ctype = escapeJavaArray(erasure(tp.normalizedTupleType)) if ctype.exists then classTagModul.select(nme.apply) .appliedToType(tp) @@ -231,6 +258,8 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context): withNoErrors(success(Literal(Constant(())))) case n: TermRef => withNoErrors(success(ref(n))) + case ts: ThisType => + withNoErrors(success(This(ts.cls))) case tp => EmptyTreeNoError case _ => diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index ba9975923df0..f7edc82270bf 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -157,7 +157,7 @@ trait TypeAssigner { else qualType.findMember(name, pre) - if reallyExists(mbr) then qualType.select(name, mbr) + if reallyExists(mbr) && NamedType.validPrefix(qualType) then qualType.select(name, mbr) else if qualType.isErroneous || name.toTermName == nme.ERROR then UnspecifiedErrorType else NoType end selectionType diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d46c85b38e9d..6289f310ac1e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -77,6 +77,9 @@ object Typer { /** Indicates that an expression is explicitly ascribed to [[Unit]] type. */ val AscribedToUnit = new Property.StickyKey[Unit] + /** Tree adaptation lost fidelity; this attachment preserves the original tree. */ + val AdaptedTree = new Property.StickyKey[tpd.Tree] + /** An attachment on a Select node with an `apply` field indicating that the `apply` * was inserted by the Typer. */ @@ -629,32 +632,40 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val checkedType = checkNotShadowed(ownType) val tree1 = checkedType match case checkedType: NamedType if !prefixIsElidable(checkedType) => - ref(checkedType).withSpan(tree.span) + ref(checkedType).withSpan(tree.span).withAttachmentsFrom(tree) case _ => tree.withType(checkedType) val tree2 = toNotNullTermRef(tree1, pt) checkLegalValue(tree2, pt) tree2 - def isLocalExtensionMethodRef: Boolean = rawType match - case rawType: TermRef => - rawType.denot.hasAltWith(_.symbol.is(ExtensionMethod)) - && !pt.isExtensionApplyProto - && { + // extensionParam + def leadParamOf(m: SymDenotation): Symbol = + def leadParam(paramss: List[List[Symbol]]): Symbol = paramss match + case (param :: _) :: paramss if param.isType => leadParam(paramss) + case _ :: (param :: Nil) :: _ if m.name.isRightAssocOperatorName => param + case (param :: Nil) :: _ => param + case _ => NoSymbol + leadParam(m.rawParamss) + + val localExtensionSelection: untpd.Tree = + var select: untpd.Tree = EmptyTree + if ctx.mode.is(Mode.InExtensionMethod) then + rawType match + case rawType: TermRef + if rawType.denot.hasAltWith(_.symbol.is(ExtensionMethod)) && !pt.isExtensionApplyProto => val xmethod = ctx.owner.enclosingExtensionMethod - rawType.denot.hasAltWith { alt => - alt.symbol.is(ExtensionMethod) - && alt.symbol.extensionParam.span == xmethod.extensionParam.span - } - } - case _ => - false + val xparam = leadParamOf(xmethod) + if rawType.denot.hasAltWith: alt => + alt.symbol.is(ExtensionMethod) + && alt.symbol.extensionParam.span == xparam.span // forces alt.symbol (which might be xmethod) + then + select = untpd.cpy.Select(tree)(untpd.ref(xparam), name) + case _ => + select - if ctx.mode.is(Mode.InExtensionMethod) && isLocalExtensionMethodRef then - val xmethod = ctx.owner.enclosingExtensionMethod - val qualifier = untpd.ref(xmethod.extensionParam.termRef) - val selection = untpd.cpy.Select(tree)(qualifier, name) - typed(selection, pt) + if !localExtensionSelection.isEmpty then + typed(localExtensionSelection, pt) else if rawType.exists then val ref = setType(ensureAccessible(rawType, superAccess = false, tree.srcPos)) if ref.symbol.name != name then @@ -727,10 +738,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // Otherwise, map combinations of A *: B *: .... EmptyTuple with nesting levels <= 22 // to the Tuple class of the right arity and select from that one def trySmallGenericTuple(qual: Tree, withCast: Boolean) = - if qual.tpe.isSmallGenericTuple then + val tp = qual.tpe.widenTermRefExpr + val tpNormalized = tp.normalizedTupleType + if tp ne tpNormalized then if withCast then - val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil) - typedSelectWithAdapt(tree, pt, qual.cast(defn.tupleType(elems))) + typedSelectWithAdapt(tree, pt, qual.cast(tpNormalized)) else typedSelectWithAdapt(tree, pt, qual) else EmptyTree @@ -1439,11 +1451,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else typedFunctionValue(tree, pt) def typedFunctionType(tree: untpd.Function, pt: Type)(using Context): Tree = { - val untpd.Function(args, body) = tree - body match - case untpd.CapturesAndResult(refs, result) => + val untpd.Function(args, result) = tree + result match + case untpd.CapturesAndResult(refs, result1) => return typedUnadapted(untpd.makeRetaining( - cpy.Function(tree)(args, result), refs, tpnme.retains), pt) + cpy.Function(tree)(args, result1), refs, tpnme.retains), pt) case _ => var (funFlags, erasedParams) = tree match { case tree: untpd.FunctionWithMods => (tree.mods.flags, tree.erasedParams) @@ -1455,22 +1467,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val isImpure = funFlags.is(Impure) /** Typechecks dependent function type with given parameters `params` */ - def typedDependent(params: List[untpd.ValDef])(using Context): Tree = - val fixThis = new untpd.UntypedTreeMap: - // pretype all references of this in outer context, - // so that they do not refer to the refined type being constructed - override def transform(tree: untpd.Tree)(using Context): untpd.Tree = tree match - case This(id) => untpd.TypedSplice(typedExpr(tree)(using ctx.outer)) - case _ => super.transform(tree) - + def typedDependent(params: List[untpd.ValDef], result: untpd.Tree)(using Context): Tree = val params1 = if funFlags.is(Given) then params.map(_.withAddedFlags(Given)) else params - val params2 = params1.map(fixThis.transformSub) - val params3 = params2.zipWithConserve(erasedParams) { (arg, isErased) => + val params2 = params1.zipWithConserve(erasedParams): (arg, isErased) => if isErased then arg.withAddedFlags(Erased) else arg - } - val appDef0 = untpd.DefDef(nme.apply, List(params3), body, EmptyTree).withSpan(tree.span) + val appDef0 = untpd.DefDef(nme.apply, List(params2), result, EmptyTree).withSpan(tree.span) index(appDef0 :: Nil) val appDef = typed(appDef0).asInstanceOf[DefDef] val mt = appDef.symbol.info.asInstanceOf[MethodType] @@ -1478,14 +1481,14 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer report.error(em"$mt is an illegal function type because it has inter-parameter dependencies", tree.srcPos) // Restart typechecking if there are erased classes that we want to mark erased if mt.erasedParams.zip(mt.paramInfos.map(_.isErasedClass)).exists((paramErased, classErased) => classErased && !paramErased) then - val newParams = params3.zipWithConserve(mt.paramInfos.map(_.isErasedClass)) { (arg, isErasedClass) => + val newParams = params2.zipWithConserve(mt.paramInfos.map(_.isErasedClass)) { (arg, isErasedClass) => if isErasedClass then arg.withAddedFlags(Erased) else arg } - return typedDependent(newParams) + return typedDependent(newParams, result) val core = if mt.hasErasedParams then TypeTree(defn.ErasedFunctionClass.typeRef) else - val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span) + val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(result.span) val paramTpts = appDef.termParamss.head.map(p => TypeTree(p.tpt.tpe).withSpan(p.tpt.span)) val funSym = defn.FunctionSymbol(numArgs, isContextual, isImpure) val tycon = TypeTree(funSym.typeRef) @@ -1495,19 +1498,28 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer args match { case ValDef(_, _, _) :: _ => - typedDependent(args.asInstanceOf[List[untpd.ValDef]])( + val fixThis = new untpd.UntypedTreeMap: + // pretype all references of this so that they do not refer to the + // refined type being constructed + override def transform(tree: untpd.Tree)(using Context): untpd.Tree = tree match + case This(id) => untpd.TypedSplice(typedExpr(tree)) + case _ => super.transform(tree) + + val untpd.Function(fixedArgs: List[untpd.ValDef] @unchecked, fixedResult) = + fixThis.transform(tree): @unchecked + typedDependent(fixedArgs, fixedResult)( using ctx.fresh.setOwner(newRefinedClassSymbol(tree.span)).setNewScope) case _ => if erasedParams.contains(true) then typedFunctionType(desugar.makeFunctionWithValDefs(tree, pt), pt) else val funSym = defn.FunctionSymbol(numArgs, isContextual, isImpure) - val result = typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ body), pt) + val funTpt = typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ result), pt) // if there are any erased classes, we need to re-do the typecheck. - result match + funTpt match case r: AppliedTypeTree if r.args.exists(_.tpe.isErasedClass) => typedFunctionType(desugar.makeFunctionWithValDefs(tree, pt), pt) - case _ => result + case _ => funTpt } } @@ -1698,7 +1710,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case mt: MethodType => pt.findFunctionType match { case SAMType(samMeth, samParent) - if !defn.isFunctionNType(samParent) && mt <:< samMeth => + if !ctx.erasedTypes && !defn.isFunctionNType(samParent) + && mt <:< samMeth && !mt.isImplicitMethod => if defn.isContextFunctionType(mt.resultType) then report.error( em"""Implementation restriction: cannot convert this expression to `$samParent` @@ -2483,31 +2496,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def registerNowarn(tree: Tree, mdef: untpd.Tree)(using Context): Unit = val annot = Annotations.Annotation(tree) - def argPos = annot.argument(0).getOrElse(tree).sourcePos - var verbose = false - val filters = annot.argumentConstantString(0) match - case None => annot.argument(0) match - case Some(t: Select) if t.name.is(DefaultGetterName) => - // default argument used for `@nowarn` and `@nowarn()` - List(MessageFilter.Any) - case _ => - report.warning(s"filter needs to be a compile-time constant string", argPos) - List(MessageFilter.None) - case Some("") => - List(MessageFilter.Any) - case Some("verbose") | Some("v") => - verbose = true - List(MessageFilter.Any) - case Some(s) => - WConf.parseFilters(s).left.map(parseErrors => - report.warning (s"Invalid message filter\n${parseErrors.mkString ("\n")}", argPos) - List(MessageFilter.None) - ).merge - val range = mdef.sourcePos - val sup = Suppression(tree.sourcePos, filters, range.start, range.end, verbose) - // invalid suppressions, don't report as unused - if filters == List(MessageFilter.None) then sup.markUsed() - ctx.run.nn.suppressions.addSuppression(sup) + val argPos = annot.argument(0).getOrElse(tree).sourcePos + val conf = annot.argumentConstantString(0).getOrElse: + annot.argument(0) match + case Some(t: Select) if t.name.is(DefaultGetterName) => + "" // default argument used for `@nowarn` and `@nowarn()` + case _ => + report.warning(s"filter needs to be a compile-time constant string", argPos) + "none" // not a -Wconf filter, mapped to MessageFilter.None by registerNowarn + ctx.run.nn.suppressions.registerNowarn(tree.sourcePos, mdef.span)(conf, argPos) def typedValDef(vdef: untpd.ValDef, sym: Symbol)(using Context): Tree = ctx.profiler.onTypedDef(sym) { val ValDef(name, tpt, _) = vdef @@ -2792,7 +2789,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer checkNonCyclicInherited(cls.thisType, cls.info.parents, cls.info.decls, cdef.srcPos) // check value class constraints - checkDerivedValueClass(cls, body1) + checkDerivedValueClass(cdef, cls, body1) // check PolyFunction constraints (no erased functions!) if parents1.exists(_.tpe.classSymbol eq defn.PolyFunctionClass) then @@ -3057,7 +3054,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree = { val arity = tree.trees.length if (arity <= Definitions.MaxTupleArity) - typed(desugar.smallTuple(tree).withSpan(tree.span), pt) + typed(desugar.smallTuple(tree).withSpan(tree.span).withAttachmentsFrom(tree), pt) else { val pts = pt.tupleElementTypes match @@ -3818,9 +3815,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer readapt(tree.appliedToNone) // insert () to primary constructors else errorTree(tree, em"Missing arguments for $methodStr") - case _ => tryInsertApplyOrImplicit(tree, pt, locked) { - errorTree(tree, MethodDoesNotTakeParameters(tree)) - } + case _ => + tryInsertApplyOrImplicit(tree, pt, locked): + errorTree(tree, MethodDoesNotTakeParameters(tree)) } def adaptNoArgsImplicitMethod(wtp: MethodType): Tree = { @@ -3839,6 +3836,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def addImplicitArgs(using Context) = def hasDefaultParams = methPart(tree).symbol.hasDefaultParams + def findDefaultArgument(argIndex: Int): Tree = + def appPart(t: Tree): Tree = t match + case Block(_, expr) => appPart(expr) + case Inlined(_, _, expr) => appPart(expr) + case t => t + defaultArgument(appPart(tree), n = argIndex, testOnly = false) def implicitArgs(formals: List[Type], argIndex: Int, pt: Type): List[Tree] = formals match case Nil => Nil case formal :: formals1 => @@ -3853,27 +3856,31 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer implicitArgs(formals2, argIndex + 1, pt) val arg = inferImplicitArg(formal, tree.span.endPos) + + lazy val defaultArg = findDefaultArgument(argIndex) + .showing(i"default argument: for $formal, $tree, $argIndex = $result", typr) + def argHasDefault = hasDefaultParams && !defaultArg.isEmpty + + def canProfitFromMoreConstraints = + arg.tpe.isInstanceOf[AmbiguousImplicits] + // Ambiguity could be decided by more constraints + || !isFullyDefined(formal, ForceDegree.none) && !argHasDefault + // More context might constrain type variables which could make implicit scope larger. + // But in this case we should search with additional arguments typed only if there + // is no default argument. + arg.tpe match - case failed: AmbiguousImplicits => + case failed: SearchFailureType if canProfitFromMoreConstraints => val pt1 = pt.deepenProtoTrans if (pt1 `ne` pt) && (pt1 ne sharpenedPt) && constrainResult(tree.symbol, wtp, pt1) - then implicitArgs(formals, argIndex, pt1) - else arg :: implicitArgs(formals1, argIndex + 1, pt1) + then return implicitArgs(formals, argIndex, pt1) + case _ => + + arg.tpe match + case failed: AmbiguousImplicits => + arg :: implicitArgs(formals1, argIndex + 1, pt) case failed: SearchFailureType => - lazy val defaultArg = - def appPart(t: Tree): Tree = t match - case Block(stats, expr) => appPart(expr) - case Inlined(_, _, expr) => appPart(expr) - case _ => t - defaultArgument(appPart(tree), argIndex, testOnly = false) - .showing(i"default argument: for $formal, $tree, $argIndex = $result", typr) - if !hasDefaultParams || defaultArg.isEmpty then - // no need to search further, the adapt fails in any case - // the reason why we continue inferring arguments in case of an AmbiguousImplicits - // is that we need to know whether there are further errors. - // If there are none, we have to propagate the ambiguity to the caller. - arg :: formals1.map(dummyArg) - else + if argHasDefault then // This is tricky. On the one hand, we need the defaultArg to // correctly type subsequent formal parameters in the same using // clause in case there are parameter dependencies. On the other hand, @@ -3884,6 +3891,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // `if propFail.exists` where we re-type the whole using clause with named // arguments for all implicits that were found. arg :: inferArgsAfter(defaultArg) + else + // no need to search further, the adapt fails in any case + // the reason why we continue inferring arguments in case of an AmbiguousImplicits + // is that we need to know whether there are further errors. + // If there are none, we have to propagate the ambiguity to the caller. + arg :: formals1.map(dummyArg) case _ => arg :: inferArgsAfter(arg) end implicitArgs @@ -4204,12 +4217,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer /** Adapt an expression of constant type to a different constant type `tpe`. */ def adaptConstant(tree: Tree, tpe: ConstantType): Tree = { - def lit = Literal(tpe.value).withSpan(tree.span) + def lit = Literal(tpe.value).withSpan(tree.span).withAttachment(AdaptedTree, tree) tree match { case Literal(c) => lit case tree @ Block(stats, expr) => tpd.cpy.Block(tree)(stats, adaptConstant(expr, tpe)) case tree => - if (isIdempotentExpr(tree)) lit // See discussion in phase Literalize why we demand isIdempotentExpr + if isIdempotentExpr(tree) then lit // See discussion in phase FirstTransform why we demand isIdempotentExpr else Block(tree :: Nil, lit) } } @@ -4249,7 +4262,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer && !ctx.isAfterTyper && !tree.isInstanceOf[Inlined] && !isThisTypeResult(tree) - && !tree.hasAttachment(AscribedToUnit) then + && !isAscribedToUnit(tree) + then report.warning(ValueDiscarding(tree.tpe), tree.srcPos) return tpd.Block(tree1 :: Nil, unitLiteral) diff --git a/compiler/src/dotty/tools/dotc/util/Chars.scala b/compiler/src/dotty/tools/dotc/util/Chars.scala index cde1a63f5293..4bdf6e464cd1 100644 --- a/compiler/src/dotty/tools/dotc/util/Chars.scala +++ b/compiler/src/dotty/tools/dotc/util/Chars.scala @@ -50,7 +50,7 @@ object Chars: } /** Is character a whitespace character (but not a new line)? */ - def isWhitespace(c: Char): Boolean = + inline def isWhitespace(c: Char): Boolean = c == ' ' || c == '\t' || c == CR /** Can character form part of a doc comment variable $xxx? */ diff --git a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala index ec88b5880745..cca7af0d2fa3 100644 --- a/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala +++ b/compiler/src/dotty/tools/dotc/util/ReusableInstance.scala @@ -1,7 +1,7 @@ package dotty.tools.dotc.util import scala.collection.mutable.ArrayBuffer -import scala.util.chaining.* +import dotty.tools.dotc.util.chaining.* /** A wrapper for a list of cached instances of a type `T`. * The wrapper is recursion-reentrant: several instances are kept, so diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index 246255ae3802..167af75a0a92 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -13,7 +13,7 @@ import Chars.* import scala.annotation.internal.sharable import scala.collection.mutable import scala.collection.mutable.ArrayBuffer -import scala.util.chaining.given +import dotty.tools.dotc.util.chaining.* import java.io.File.separator import java.net.URI @@ -228,8 +228,7 @@ object SourceFile { * It relies on SourceFile#virtual implementation to create the virtual file. */ def virtual(uri: URI, content: String): SourceFile = - val path = Paths.get(uri).toString - SourceFile.virtual(path, content) + SourceFile(new VirtualFile(Paths.get(uri), content.getBytes(StandardCharsets.UTF_8)), content.toCharArray) /** Returns the relative path of `source` within the `reference` path * @@ -275,12 +274,11 @@ object SourceFile { def apply(file: AbstractFile | Null, codec: Codec): SourceFile = // Files.exists is slow on Java 8 (https://rules.sonarsource.com/java/tag/performance/RSPEC-3725), - // so cope with failure; also deal with path prefix "Not a directory". + // so cope with failure. val chars = try new String(file.toByteArray, codec.charSet).toCharArray catch - case _: NoSuchFileException => Array.empty[Char] - case fse: FileSystemException if fse.getMessage.endsWith("Not a directory") => Array.empty[Char] + case _: FileSystemException => Array.empty[Char] if isScript(file, chars) then ScriptSourceFile(file, chars) diff --git a/compiler/src/dotty/tools/dotc/util/Spans.scala b/compiler/src/dotty/tools/dotc/util/Spans.scala index e1487408f36b..33346ad6da17 100644 --- a/compiler/src/dotty/tools/dotc/util/Spans.scala +++ b/compiler/src/dotty/tools/dotc/util/Spans.scala @@ -42,23 +42,26 @@ object Spans { /** The start of this span. */ def start: Int = { - assert(exists) + assert(exists, "start of NoSpan") (coords & StartEndMask).toInt } /** The end of this span */ def end: Int = { - assert(exists) + assert(exists, "end of NoSpan") ((coords >>> StartEndBits) & StartEndMask).toInt } /** The point of this span, returns start for synthetic spans */ def point: Int = { - assert(exists) + assert(exists, "point of NoSpan") val poff = pointDelta if (poff == SyntheticPointDelta) start else start + poff } + def pointMayBeIncorrect = + pointDelta == 0 && end - start >= SyntheticPointDelta + /** The difference between point and start in this span */ def pointDelta: Int = (coords >>> (StartEndBits * 2)).toInt diff --git a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala index f991005f0c43..bd5c031a65e0 100644 --- a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala +++ b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala @@ -15,7 +15,7 @@ package dotty.tools.dotc.util import scala.language.unsafeNulls import collection.mutable, mutable.ListBuffer -import scala.util.chaining.given +import dotty.tools.dotc.util.chaining.* import java.lang.System.lineSeparator object StackTraceOps: diff --git a/compiler/src/dotty/tools/dotc/util/chaining.scala b/compiler/src/dotty/tools/dotc/util/chaining.scala new file mode 100644 index 000000000000..0c61ab6e73e9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/chaining.scala @@ -0,0 +1,8 @@ + +package dotty.tools.dotc.util + +object chaining: + + extension [A](x: A) + inline def tap(inline f: A => Unit): x.type = { f(x): Unit; x } + inline def pipe[B](inline f: A => B): B = f(x) diff --git a/compiler/src/dotty/tools/io/Jar.scala b/compiler/src/dotty/tools/io/Jar.scala index b4c2024bb487..6076638bff45 100644 --- a/compiler/src/dotty/tools/io/Jar.scala +++ b/compiler/src/dotty/tools/io/Jar.scala @@ -50,7 +50,7 @@ class Jar(file: File) { def mainClass: Option[String] = manifest.map(_(Name.MAIN_CLASS)) /** The manifest-defined classpath String if available. */ def classPathString: Option[String] = - for (m <- manifest ; cp <- m.attrs.get(Name.CLASS_PATH)) yield cp + for (m <- manifest ; cp <- m.attrs.get(Name.CLASS_PATH) if !cp.trim().isEmpty()) yield cp def classPathElements: List[String] = classPathString match { case Some(s) => s.split("\\s+").toList case _ => Nil diff --git a/compiler/src/dotty/tools/io/VirtualFile.scala b/compiler/src/dotty/tools/io/VirtualFile.scala index 9d290a9b0e6a..e87faa0b86f9 100644 --- a/compiler/src/dotty/tools/io/VirtualFile.scala +++ b/compiler/src/dotty/tools/io/VirtualFile.scala @@ -40,15 +40,34 @@ class VirtualFile(val name: String, override val path: String) extends AbstractF this.content = content } + /** + * Initializes this instance with the specified path + * and a name taken from the last path element. + * + * @param path the path of the virtual file to be created + * @param content the initial contents of the virtual file + * @return the created virtual file + */ + def this(path: JPath, content: Array[Byte]) = { + this(path.getFileName().toString(), path.toString()) + this.content = content + this.jpath_ = path + } + private var content = Array.emptyByteArray + private var jpath_ : JPath = null + def absolute: AbstractFile = this - /** Returns null. */ - def jpath: JPath = null + /** Returns path, which might be a non-existing file or null. */ + def jpath: JPath = jpath_ override def sizeOption: Option[Int] = Some(content.length) + /** Always returns true, even if jpath is a non-existing file. */ + override def exists: Boolean = true + def input : InputStream = new ByteArrayInputStream(content) override def output: OutputStream = { diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index 6c9f95d4dca2..2b7740152fa4 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -52,6 +52,27 @@ object Load { val command: String = ":load" } +/** `:require` is a deprecated alias for :jar` + */ +case class Require(path: String) extends Command +object Require { + val command: String = ":require" +} + +/** `:jar ` adds a jar to the classpath + */ +case class JarCmd(path: String) extends Command +object JarCmd { + val command: String = ":jar" +} + +/** `:kind ` display the kind of a type. see also :help kind + */ +case class KindOf(expr: String) extends Command +object KindOf { + val command: String = ":kind" +} + /** To find out the type of an expression you may simply do: * * ``` @@ -93,6 +114,12 @@ object Reset { val command: String = ":reset" } +/** `:sh ` run a shell command (result is implicitly => List[String]) */ +case class Sh(expr: String) extends Command +object Sh { + val command: String = ":sh" +} + /** Toggle automatic printing of results */ case object Silent extends Command: val command: String = ":silent" @@ -138,10 +165,14 @@ object ParseResult { Help.command -> (_ => Help), Reset.command -> (arg => Reset(arg)), Imports.command -> (_ => Imports), + JarCmd.command -> (arg => JarCmd(arg)), + KindOf.command -> (arg => KindOf(arg)), Load.command -> (arg => Load(arg)), + Require.command -> (arg => Require(arg)), TypeOf.command -> (arg => TypeOf(arg)), DocOf.command -> (arg => DocOf(arg)), Settings.command -> (arg => Settings(arg)), + Sh.command -> (arg => Sh(arg)), Silent.command -> (_ => Silent), ) diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 5c8dcc2616b7..e34bb229a727 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -11,16 +11,16 @@ import dotty.tools.dotc.core.Phases.Phase import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.reporting.Diagnostic -import dotty.tools.dotc.transform.PostTyper +import dotty.tools.dotc.transform.{CheckUnused, CheckShadowing, PostTyper} import dotty.tools.dotc.typer.ImportInfo.{withRootImports, RootRef} import dotty.tools.dotc.typer.TyperPhase import dotty.tools.dotc.util.Spans.* import dotty.tools.dotc.util.{ParsedComment, Property, SourceFile} import dotty.tools.dotc.{CompilationUnit, Compiler, Run} import dotty.tools.repl.results.* +import dotty.tools.dotc.util.chaining.* import scala.collection.mutable -import scala.util.chaining.given /** This subclass of `Compiler` is adapted for use in the REPL. * @@ -36,6 +36,7 @@ class ReplCompiler extends Compiler: List(Parser()), List(ReplPhase()), List(TyperPhase(addRootImports = false)), + List(CheckUnused.PostTyper(), CheckShadowing()), List(CollectTopLevelImports()), List(PostTyper()), ) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index d7ba2d3ce193..cfbacf0589e1 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets import dotty.tools.dotc.ast.Trees.* import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.classpath.ClassPathFactory import dotty.tools.dotc.config.CommandLineParser.tokenize import dotty.tools.dotc.config.Properties.{javaVersion, javaVmName, simpleVersionString} import dotty.tools.dotc.core.Contexts.* @@ -21,6 +22,7 @@ import dotty.tools.dotc.core.NameOps.* import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Symbols.{Symbol, defn} +import dotty.tools.dotc.core.SymbolLoaders import dotty.tools.dotc.interfaces import dotty.tools.dotc.interactive.Completion import dotty.tools.dotc.printing.SyntaxHighlighting @@ -38,6 +40,7 @@ import org.jline.reader.* import scala.annotation.tailrec import scala.collection.mutable import scala.jdk.CollectionConverters.* +import scala.tools.asm.ClassReader import scala.util.control.NonFatal import scala.util.Using @@ -139,7 +142,9 @@ class ReplDriver(settings: Array[String], * * Possible reason for unsuccessful run are raised flags in CLI like --help or --version */ - final def tryRunning = if shouldStart then runUntilQuit() + final def tryRunning = if shouldStart then + if rootCtx.settings.replQuitAfterInit.value(using rootCtx) then initialState + else runUntilQuit() /** Run REPL with `state` until `:quit` command found * @@ -511,6 +516,65 @@ class ReplDriver(settings: Array[String], state } + case Require(path) => + out.println(":require is no longer supported, but has been replaced with :jar. Please use :jar") + state + + case JarCmd(path) => + val jarFile = AbstractFile.getDirectory(path) + if (jarFile == null) + out.println(s"""Cannot add "$path" to classpath.""") + state + else + def flatten(f: AbstractFile): Iterator[AbstractFile] = + if (f.isClassContainer) f.iterator.flatMap(flatten) + else Iterator(f) + + def tryClassLoad(classFile: AbstractFile): Option[String] = { + val input = classFile.input + try { + val reader = new ClassReader(input) + val clsName = reader.getClassName.replace('/', '.') + rendering.myClassLoader.loadClass(clsName) + Some(clsName) + } catch + case _: ClassNotFoundException => None + finally { + input.close() + } + } + + try { + val entries = flatten(jarFile) + + val existingClass = entries.filter(_.extension == "class").find(tryClassLoad(_).isDefined) + if (existingClass.nonEmpty) + out.println(s"The path '$path' cannot be loaded, it contains a classfile that already exists on the classpath: ${existingClass.get}") + else inContext(state.context): + val jarClassPath = ClassPathFactory.newClassPath(jarFile) + val prevOutputDir = ctx.settings.outputDir.value + + // add to compiler class path + ctx.platform.addToClassPath(jarClassPath) + SymbolLoaders.mergeNewEntries(defn.RootClass, ClassPath.RootPackage, jarClassPath, ctx.platform.classPath) + + // new class loader with previous output dir and specified jar + val prevClassLoader = rendering.classLoader() + val jarClassLoader = fromURLsParallelCapable( + jarClassPath.asURLs, prevClassLoader) + rendering.myClassLoader = new AbstractFileClassLoader( + prevOutputDir, jarClassLoader) + + out.println(s"Added '$path' to classpath.") + } catch { + case e: Throwable => + out.println(s"Failed to load '$path' to classpath: ${e.getMessage}") + } + state + + case KindOf(expr) => + out.println(s"""The :kind command is not currently supported.""") + state case TypeOf(expr) => expr match { case "" => out.println(s":type ") @@ -533,6 +597,10 @@ class ReplDriver(settings: Array[String], } state + case Sh(expr) => + out.println(s"""The :sh command is deprecated. Use `import scala.sys.process._` and `"command".!` instead.""") + state + case Settings(arg) => arg match case "" => given ctx: Context = state.context diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index d4f2bdd0991b..87a865f81e1a 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -454,7 +454,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler withDefaultPos(tpd.ref(tp).asInstanceOf[tpd.RefTree]) def apply(sym: Symbol): Ref = assert(sym.isTerm, s"expected a term symbol, but received $sym") - val refTree = tpd.ref(sym) match + val refTree = tpd.generalisedRef(sym) match case t @ tpd.This(ident) => // not a RefTree, so we need to work around this - issue #19732 // ident in `This` can be a TypeIdent of sym, so we manually prepare the ref here, // knowing that the owner is actually `This`. @@ -805,7 +805,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler object BlockTypeTest extends TypeTest[Tree, Block]: def unapply(x: Tree): Option[Block & x.type] = x match - case x: (tpd.Block & x.type) => Some(x) + case x: (tpd.Block & x.type) if x.isTerm => Some(x) case _ => None end BlockTypeTest @@ -1390,7 +1390,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler object TypeBlockTypeTest extends TypeTest[Tree, TypeBlock]: def unapply(x: Tree): Option[TypeBlock & x.type] = x match - case tpt: (tpd.Block & x.type) => Some(tpt) + case tpt: (tpd.Block & x.type) if x.isType => Some(tpt) case _ => None end TypeBlockTypeTest @@ -1795,7 +1795,15 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def termSymbol: Symbol = self.termSymbol def isSingleton: Boolean = self.isSingleton def memberType(member: Symbol): TypeRepr = - member.info.asSeenFrom(self, member.owner) + // we replace thisTypes here to avoid resolving otherwise unstable prefixes into Nothing + val memberInfo = + if self.typeSymbol.isClassDef then + member.info.substThis(self.classSymbol.asClass, self) + else + member.info + memberInfo + .asSeenFrom(self, member.owner) + def baseClasses: List[Symbol] = self.baseClasses def baseType(cls: Symbol): TypeRepr = self.baseType(cls) def derivesFrom(cls: Symbol): Boolean = self.derivesFrom(cls) @@ -2411,7 +2419,12 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler end StringConstantTypeTest object StringConstant extends StringConstantModule: - def apply(x: String): StringConstant = dotc.core.Constants.Constant(x) + def apply(x: String): StringConstant = + require(x != null, "value of StringConstant cannot be `null`") + // A `null` constant must be represented as a `NullConstant`, c.f. a + // constant with `tag == NullTag`, which is not a `StringConstant`. + // See issue 23008. + dotc.core.Constants.Constant(x) def unapply(constant: StringConstant): Some[String] = Some(constant.stringValue) end StringConstant @@ -2448,7 +2461,17 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler object ClassOfConstant extends ClassOfConstantModule: def apply(x: TypeRepr): ClassOfConstant = - // TODO check that the type is a valid class when creating this constant or let Ycheck do it? + // We only check if the supplied TypeRepr is valid if it contains an Array, + // as so far only that Array could cause issues + def correctTypeApplicationForArray(typeRepr: TypeRepr): Boolean = + val isArray = typeRepr.typeSymbol != dotc.core.Symbols.defn.ArrayClass + typeRepr match + case AppliedType(_, targs) if !targs.isEmpty => true + case _ => isArray + xCheckMacroAssert( + correctTypeApplicationForArray(x), + "Illegal empty Array type constructor. Please supply a type parameter." + ) dotc.core.Constants.Constant(x) def unapply(constant: ClassOfConstant): Some[TypeRepr] = Some(constant.typeValue) end ClassOfConstant @@ -2734,7 +2757,33 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def memberTypes: List[Symbol] = self.typeRef.decls.filter(_.isType) def typeMembers: List[Symbol] = - lookupPrefix.typeMembers.map(_.symbol).toList + // lookupPrefix.typeMembers currently returns a Set wrapped into a unsorted Seq, + // so we try to sort that here (see discussion: https://github.com/scala/scala3/issues/22472), + // without adding too much of a performance hit. + // It first sorts by parents, then for type params by their positioning, then for members + // derived from declarations it sorts them by their name lexicographically + val parentsMap = lookupPrefix.sortedParents.map(_.typeSymbol).zipWithIndex.toList.toMap + val unsortedTypeMembers = lookupPrefix.typeMembers.map(_.symbol).filter(_.exists).toList + unsortedTypeMembers.sortWith { + case (typeA, typeB) => + val msg = "Unknown type member found. Please consider reporting the issue to the compiler. " + assert(parentsMap.contains(typeA.owner), msg) + assert(parentsMap.contains(typeB.owner), msg) + val parentPlacementA = parentsMap(typeA.owner) + val parentPlacementB = parentsMap(typeB.owner) + if (parentPlacementA == parentPlacementB) then + if typeA.isTypeParam && typeB.isTypeParam then + // put type params at the beginning (and sort them by declaration order) + val pl = typeA.owner + val typeParamPositionMap = pl.typeParams.map(_.asInstanceOf[Symbol]).zipWithIndex.toMap + typeParamPositionMap(typeA) < typeParamPositionMap(typeB) + else if typeA.isTypeParam then true + else if typeB.isTypeParam then false + else + // sort by name lexicographically + typeA.name.toString().compareTo(typeB.name.toString()) < 0 + else parentPlacementA < parentPlacementB + }.map(_.asInstanceOf[Symbol]) def declarations: List[Symbol] = self.typeRef.info.decls.toList diff --git a/compiler/src/scala/quoted/runtime/impl/printers/Extractors.scala b/compiler/src/scala/quoted/runtime/impl/printers/Extractors.scala index 726f2daf1788..a66dabc542e3 100644 --- a/compiler/src/scala/quoted/runtime/impl/printers/Extractors.scala +++ b/compiler/src/scala/quoted/runtime/impl/printers/Extractors.scala @@ -1,6 +1,7 @@ package scala.quoted package runtime.impl.printers +import scala.collection.mutable import scala.quoted.* object Extractors { @@ -67,6 +68,12 @@ object Extractors { import quotes.reflect.* private val sb: StringBuilder = new StringBuilder + private var recTypeCounter = 0 + private val recTypeIds = mutable.Map.empty[RecursiveType, Int] + + private def nextRecTypeId(): Int = + recTypeCounter += 1 + recTypeCounter def result(): String = sb.result() @@ -226,9 +233,14 @@ object Extractors { case SuperType(thistpe, supertpe) => this += "SuperType(" += thistpe += ", " += supertpe += ")" case RecursiveThis(binder) => - this += "RecursiveThis(" += binder += ")" - case RecursiveType(underlying) => - this += "RecursiveType(" += underlying += ")" + val id = recTypeIds.getOrElse(binder, -1) + if (id == -1) + this += "RecursiveThis(" += binder += ")" + else + this += "RecursiveThis( + val id = recTypeIds.getOrElseUpdate(rt, nextRecTypeId()) + this += "RecursiveType(rec" += id += " => " += underlying += ")" case MethodType(argNames, argTypes, resType) => this += "MethodType(" ++= argNames += ", " ++= argTypes += ", " += resType += ")" case PolyType(argNames, argBounds, resType) => diff --git a/compiler/test-resources/jars/MyLibrary.scala b/compiler/test-resources/jars/MyLibrary.scala new file mode 100644 index 000000000000..bd752d2df864 --- /dev/null +++ b/compiler/test-resources/jars/MyLibrary.scala @@ -0,0 +1,8 @@ +/** + * JAR used for testing repl :jar + * Generated using: mkdir out; scalac -d out MyLibrary.scala; jar cf mylibrary.jar -C out . + */ +package mylibrary + +object Utils: + def greet(name: String): String = s"Hello, $name!" diff --git a/compiler/test-resources/jars/MyLibrary2.scala b/compiler/test-resources/jars/MyLibrary2.scala new file mode 100644 index 000000000000..df5a944c0bcf --- /dev/null +++ b/compiler/test-resources/jars/MyLibrary2.scala @@ -0,0 +1,9 @@ +/** + * JAR used for testing repl :jar + * Generated using: mkdir out2; scalac -d out MyLibrary2.scala; jar cf mylibrary2.jar -C out2 . + */ +package mylibrary2 + +object Utils2: + def greet(name: String): String = s"Greetings, $name!" + diff --git a/compiler/test-resources/jars/mylibrary.jar b/compiler/test-resources/jars/mylibrary.jar new file mode 100644 index 000000000000..4537e10ee491 Binary files /dev/null and b/compiler/test-resources/jars/mylibrary.jar differ diff --git a/compiler/test-resources/jars/mylibrary2.jar b/compiler/test-resources/jars/mylibrary2.jar new file mode 100644 index 000000000000..1d6584bdf545 Binary files /dev/null and b/compiler/test-resources/jars/mylibrary2.jar differ diff --git a/compiler/test-resources/repl/i21655 b/compiler/test-resources/repl/i21655 new file mode 100644 index 000000000000..57b09bfad32d --- /dev/null +++ b/compiler/test-resources/repl/i21655 @@ -0,0 +1,2 @@ +scala>:kind +The :kind command is not currently supported. \ No newline at end of file diff --git a/compiler/test-resources/repl/i21657 b/compiler/test-resources/repl/i21657 new file mode 100644 index 000000000000..fa2eec3ac891 --- /dev/null +++ b/compiler/test-resources/repl/i21657 @@ -0,0 +1,2 @@ +scala>:sh +The :sh command is deprecated. Use `import scala.sys.process._` and `"command".!` instead. \ No newline at end of file diff --git a/compiler/test-resources/repl/jar-command b/compiler/test-resources/repl/jar-command new file mode 100644 index 000000000000..b0ded22d3654 --- /dev/null +++ b/compiler/test-resources/repl/jar-command @@ -0,0 +1,13 @@ +scala> val z = 1 +val z: Int = 1 + +scala>:jar sbt-test/source-dependencies/canon/actual/a.jar +Added 'sbt-test/source-dependencies/canon/actual/a.jar' to classpath. + +scala> import A.x + +scala> x +val res0: Int = 3 + +scala> z +val res1: Int = 1 diff --git a/compiler/test-resources/repl/jar-errors b/compiler/test-resources/repl/jar-errors new file mode 100644 index 000000000000..7d5720fcb233 --- /dev/null +++ b/compiler/test-resources/repl/jar-errors @@ -0,0 +1,11 @@ +scala>:jar path/does/not/exist +Cannot add "path/does/not/exist" to classpath. + +scala>:jar sbt-test/source-dependencies/canon/actual/a.jar +Added 'sbt-test/source-dependencies/canon/actual/a.jar' to classpath. + +scala>:jar sbt-test/source-dependencies/canon/actual/a.jar +The path 'sbt-test/source-dependencies/canon/actual/a.jar' cannot be loaded, it contains a classfile that already exists on the classpath: sbt-test/source-dependencies/canon/actual/a.jar(A.class) + +scala>:require sbt-test/source-dependencies/canon/actual/a.jar +:require is no longer supported, but has been replaced with :jar. Please use :jar \ No newline at end of file diff --git a/compiler/test-resources/repl/jar-multiple b/compiler/test-resources/repl/jar-multiple new file mode 100644 index 000000000000..453ccc40dbf6 --- /dev/null +++ b/compiler/test-resources/repl/jar-multiple @@ -0,0 +1,32 @@ +scala> val z = 1 +val z: Int = 1 + +scala>:jar compiler/test-resources/jars/mylibrary.jar +Added 'compiler/test-resources/jars/mylibrary.jar' to classpath. + +scala> import mylibrary.Utils + +scala> Utils.greet("Alice") +val res0: String = Hello, Alice! + +scala>:jar compiler/test-resources/jars/mylibrary2.jar +Added 'compiler/test-resources/jars/mylibrary2.jar' to classpath. + +scala> import mylibrary2.Utils2 + +scala> Utils2.greet("Alice") +val res1: String = Greetings, Alice! + +scala> Utils.greet("Alice") +val res2: String = Hello, Alice! + +scala> import mylibrary.Utils.greet + +scala> greet("Tom") +val res3: String = Hello, Tom! + +scala> Utils.greet("Alice") +val res4: String = Hello, Alice! + +scala> z +val res5: Int = 1 diff --git a/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist b/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist new file mode 100644 index 000000000000..03b020db64d9 --- /dev/null +++ b/compiler/test/dotc/neg-init-global-scala2-library-tasty.excludelist @@ -0,0 +1,21 @@ +## See #18882 +patmat.scala +t9312.scala +unapplySeq-implicit-arg.scala +unapplySeq-implicit-arg2.scala +unapplySeq-implicit-arg3.scala +ScalaCheck.scala +mutable-read8.scala +TypeCast.scala +global-cycle8.scala +global-cycle6.scala +i12544b.scala +t9360.scala +mutable-array.scala +patmat-unapplySeq2.scala +line-spacing.scala +global-list.scala +t5366.scala +mutable-read7.scala +t9115.scala +Color.scala \ No newline at end of file diff --git a/compiler/test/dotc/neg-scala2-library-tasty.excludelist b/compiler/test/dotc/neg-scala2-library-tasty.excludelist new file mode 100644 index 000000000000..d46a021ddd50 --- /dev/null +++ b/compiler/test/dotc/neg-scala2-library-tasty.excludelist @@ -0,0 +1,2 @@ +i8752.scala +f-interpolator-neg.scala # Additional: A pure expression does nothing in statement position diff --git a/compiler/test/dotc/patmat-exhaustivity-scala2-library-tasty.excludelist b/compiler/test/dotc/patmat-exhaustivity-scala2-library-tasty.excludelist new file mode 100644 index 000000000000..6f1717d532fd --- /dev/null +++ b/compiler/test/dotc/patmat-exhaustivity-scala2-library-tasty.excludelist @@ -0,0 +1,4 @@ +t7746.scala # order of exhaustivity suggestions differs +t4408.scala # order of exhaustivity suggestions differs +patmat-ortype.scala # order of exhaustivity suggestions differs +i13003.scala # order of exhaustivity suggestions differs diff --git a/compiler/test/dotc/pos-from-tasty.blacklist b/compiler/test/dotc/pos-from-tasty.excludelist similarity index 100% rename from compiler/test/dotc/pos-from-tasty.blacklist rename to compiler/test/dotc/pos-from-tasty.excludelist diff --git a/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist b/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist new file mode 100644 index 000000000000..eb60fc3b7c14 --- /dev/null +++ b/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist @@ -0,0 +1,5 @@ +## See #18882 +patmat.scala +patmat-interpolator.scala +unapplySeq-implicit-arg-pos.scala +global-cycle11.scala diff --git a/compiler/test/dotc/pos-test-pickling.blacklist b/compiler/test/dotc/pos-test-pickling.excludelist similarity index 74% rename from compiler/test/dotc/pos-test-pickling.blacklist rename to compiler/test/dotc/pos-test-pickling.excludelist index 1cb73da4c2eb..1aae6f88e55a 100644 --- a/compiler/test/dotc/pos-test-pickling.blacklist +++ b/compiler/test/dotc/pos-test-pickling.excludelist @@ -24,16 +24,22 @@ t5031_2.scala i16997.scala i7414.scala i17588.scala +i8300.scala i9804.scala i13433.scala i16649-irrefutable.scala strict-pattern-bindings-3.0-migration.scala +i17186b.scala +i11982a.scala i17255 i17735.scala # Tree is huge and blows stack for printing Text i7034.scala +# Causes cyclic reference by interacting with compiler stdlib types +stdlib + # Stale symbol: package object scala seqtype-cycle @@ -57,8 +63,15 @@ i6505.scala i15158.scala i15155.scala i15827.scala +i17149.scala +tuple-fold.scala +mt-redux-norm.perspective.scala i18211.scala +10867.scala +named-tuples1.scala i20897.scala +i20512.scala +i22645b.scala # Opaque type i5720.scala @@ -95,7 +108,7 @@ i13842.scala # Position change under captureChecking boxmap-paper.scala -# Function types print differnt after unpickling since test mispredicts Feature.preFundsEnabled +# Function types print different after unpickling since test mispredicts Feature.preFundsEnabled caps-universal.scala # GADT cast applied to singleton type difference @@ -103,6 +116,7 @@ i4176-gadt.scala # GADT difference i13974a.scala +i15867.scala java-inherited-type1 @@ -115,6 +129,19 @@ i19955a.scala i19955b.scala i20053b.scala +# alias types at different levels of dereferencing +parsercombinators-givens.scala +parsercombinators-givens-2.scala +parsercombinators-ctx-bounds.scala +parsercombinators-this.scala +parsercombinators-arrow.scala +parsercombinators-new-syntax.scala +hylolib-deferred-given +hylolib-cb +hylolib + +# typecheckErrors method unpickling +i21415.scala # LTS specific i21390.TrieMap.scala diff --git a/compiler/test/dotc/run-from-tasty.blacklist b/compiler/test/dotc/run-from-tasty.excludelist similarity index 100% rename from compiler/test/dotc/run-from-tasty.blacklist rename to compiler/test/dotc/run-from-tasty.excludelist diff --git a/compiler/test/dotc/run-macros-scala2-library-tasty.excludelist b/compiler/test/dotc/run-macros-scala2-library-tasty.excludelist new file mode 100644 index 000000000000..6fdfccf7646c --- /dev/null +++ b/compiler/test/dotc/run-macros-scala2-library-tasty.excludelist @@ -0,0 +1,5 @@ +# Checkfile differences for equivalent type +tasty-extractors-1 +tasty-extractors-2 +tasty-extractors-types +type-print diff --git a/compiler/test/dotc/run-test-pickling.blacklist b/compiler/test/dotc/run-test-pickling.excludelist similarity index 95% rename from compiler/test/dotc/run-test-pickling.blacklist rename to compiler/test/dotc/run-test-pickling.excludelist index 32fa44d0cb20..1597487da668 100644 --- a/compiler/test/dotc/run-test-pickling.blacklist +++ b/compiler/test/dotc/run-test-pickling.excludelist @@ -27,7 +27,6 @@ tuple-zip.scala tuples1.scala tuples1a.scala tuples1b.scala -typeCheckErrors.scala typeclass-derivation-doc-example.scala typeclass-derivation1.scala typeclass-derivation2.scala @@ -45,8 +44,9 @@ t6138-2 i12656.scala trait-static-forwarder i17255 +named-tuples-strawman-2.scala # typecheckErrors method unpickling typeCheckErrors.scala i18150.scala - +i22968.scala diff --git a/compiler/test/dotty/Properties.scala b/compiler/test/dotty/Properties.scala index cc47303d5468..4830f1d7b24c 100644 --- a/compiler/test/dotty/Properties.scala +++ b/compiler/test/dotty/Properties.scala @@ -19,7 +19,6 @@ object Properties { /** Are we running on the CI? */ val isRunByCI: Boolean = sys.env.isDefinedAt("DOTTY_CI_RUN") - || sys.env.isDefinedAt("DRONE") // TODO remove this when we drop Drone val testCache: Path = sys.env.get("DOTTY_TEST_CACHE").map(Paths.get(_)).getOrElse { diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala index 68a260de81e4..15cb0c741ede 100644 --- a/compiler/test/dotty/tools/TestSources.scala +++ b/compiler/test/dotty/tools/TestSources.scala @@ -11,28 +11,27 @@ object TestSources { // pos tests lists - def posFromTastyBlacklistFile: String = "compiler/test/dotc/pos-from-tasty.blacklist" - def posTestPicklingBlacklistFile: String = "compiler/test/dotc/pos-test-pickling.blacklist" + def posFromTastyExcludelistFile: String = "compiler/test/dotc/pos-from-tasty.excludelist" + def posTestPicklingExcludelistFile: String = "compiler/test/dotc/pos-test-pickling.excludelist" def posTestRecheckExcludesFile: String = "compiler/test/dotc/pos-test-recheck.excludes" def posLazyValsAllowlistFile: String = "compiler/test/dotc/pos-lazy-vals-tests.allowlist" def posLintingAllowlistFile: String = "compiler/test/dotc/pos-linting.allowlist" - def posFromTastyBlacklisted: List[String] = loadList(posFromTastyBlacklistFile) - def posTestPicklingBlacklisted: List[String] = loadList(posTestPicklingBlacklistFile) + def posFromTastyExcludelisted: List[String] = loadList(posFromTastyExcludelistFile) + def posTestPicklingExcludelisted: List[String] = loadList(posTestPicklingExcludelistFile) def posTestRecheckExcluded: List[String] = loadList(posTestRecheckExcludesFile) def posLazyValsAllowlist: List[String] = loadList(posLazyValsAllowlistFile) def posLintingAllowlist: List[String] = loadList(posLintingAllowlistFile) // run tests lists - def runFromTastyBlacklistFile: String = "compiler/test/dotc/run-from-tasty.blacklist" - def runTestPicklingBlacklistFile: String = "compiler/test/dotc/run-test-pickling.blacklist" + def runFromTastyExcludelistFile: String = "compiler/test/dotc/run-from-tasty.excludelist" + def runTestPicklingExcludelistFile: String = "compiler/test/dotc/run-test-pickling.excludelist" def runTestRecheckExcludesFile: String = "compiler/test/dotc/run-test-recheck.excludes" def runLazyValsAllowlistFile: String = "compiler/test/dotc/run-lazy-vals-tests.allowlist" - - def runFromTastyBlacklisted: List[String] = loadList(runFromTastyBlacklistFile) - def runTestPicklingBlacklisted: List[String] = loadList(runTestPicklingBlacklistFile) + def runFromTastyExcludelisted: List[String] = loadList(runFromTastyExcludelistFile) + def runTestPicklingExcludelisted: List[String] = loadList(runTestPicklingExcludelistFile) def runTestRecheckExcluded: List[String] = loadList(runTestRecheckExcludesFile) def runLazyValsAllowlist: List[String] = loadList(runLazyValsAllowlistFile) diff --git a/compiler/test/dotty/tools/backend/jvm/ClassfileParserTest.scala b/compiler/test/dotty/tools/backend/jvm/ClassfileParserTest.scala new file mode 100644 index 000000000000..9263103f1712 --- /dev/null +++ b/compiler/test/dotty/tools/backend/jvm/ClassfileParserTest.scala @@ -0,0 +1,57 @@ +package dotty.tools.backend.jvm + +// painful to do Java reflection stuff without this +import scala.language.unsafeNulls + +import org.junit.Assert.assertEquals +import org.junit.Test + +import java.lang.reflect.Member + +class ClassfileParserTest { + @Test + def noConstantPoolLag(): Unit = { + def constNames(ms: List[Member]) = ms.collect { + case f if f.getName.startsWith("CONSTANT_") => f.getName + }.sorted + + val toDotc = Map( + "CONSTANT_INTERFACE_METHODREF" -> "CONSTANT_INTFMETHODREF", + "CONSTANT_INVOKE_DYNAMIC" -> "CONSTANT_INVOKEDYNAMIC", + "CONSTANT_METHOD_HANDLE" -> "CONSTANT_METHODHANDLE", + "CONSTANT_METHOD_TYPE" -> "CONSTANT_METHODTYPE", + "CONSTANT_NAME_AND_TYPE" -> "CONSTANT_NAMEANDTYPE", + ).withDefault(x => x) + + val asmConsts = constNames(Class.forName("scala.tools.asm.Symbol").getDeclaredFields.toList) + .map(_.stripSuffix("_TAG")) + .map(toDotc) + .::("CONSTANT_UNICODE") + .sorted + // in the Scala 2 version of this test, we also use Java reflection to get the constant + // names out of ClassfileConstants. in Dotty, the constants are `inline val`s, invisible + // to Java reflection, so we hardcode them here + assertEquals(asmConsts, List( + // do not add to this list without also making the corresponding change + // in ClassfileConstants! that would defeat the purpose of the test + "CONSTANT_CLASS", + "CONSTANT_DOUBLE", + "CONSTANT_DYNAMIC", + "CONSTANT_FIELDREF", + "CONSTANT_FLOAT", + "CONSTANT_INTEGER", + "CONSTANT_INTFMETHODREF", + "CONSTANT_INVOKEDYNAMIC", + "CONSTANT_LONG", + "CONSTANT_METHODHANDLE", + "CONSTANT_METHODREF", + "CONSTANT_METHODTYPE", + "CONSTANT_MODULE", + "CONSTANT_NAMEANDTYPE", + "CONSTANT_PACKAGE", + "CONSTANT_STRING", + "CONSTANT_UNICODE", + "CONSTANT_UTF8", + )) + } +} diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index f10e159beb86..9571e80d5ccd 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -69,6 +69,7 @@ class CompilationTests { compileFile("tests/rewrites/i17399.scala", unindentOptions.and("-rewrite")), compileFile("tests/rewrites/i20002.scala", defaultOptions.and("-indent", "-rewrite")), compileFile("tests/rewrites/i21382.scala", defaultOptions.and("-indent", "-rewrite")), + compileFile("tests/rewrites/unused.scala", defaultOptions.and("-rewrite", "-Wunused:all")), ).checkRewrites() } @@ -170,8 +171,8 @@ class CompilationTests { @Test def pickling: Unit = { implicit val testGroup: TestGroup = TestGroup("testPickling") aggregateTests( - compileFilesInDir("tests/pos", picklingOptions, FileFilter.exclude(TestSources.posTestPicklingBlacklisted)), - compileFilesInDir("tests/run", picklingOptions, FileFilter.exclude(TestSources.runTestPicklingBlacklisted)) + compileFilesInDir("tests/pos", picklingOptions, FileFilter.exclude(TestSources.posTestPicklingExcludelisted)), + compileFilesInDir("tests/run", picklingOptions, FileFilter.exclude(TestSources.runTestPicklingExcludelisted)) ).checkCompile() } @@ -234,6 +235,54 @@ class CompilationTests { tests.foreach(_.delete()) } + + /* This tests for errors in the program's TASTy trees. + * The test consists of three files: (a) v1/A, (b) v1/B, and (c) v0/A. (a) and (b) are + * compatible, but (b) and (c) are not. If (b) and (c) are compiled together, there should be + * an error when reading the files' TASTy trees. */ + locally { + val tastyErrorGroup = TestGroup("checkInit/tasty-error/val-or-defdef") + val tastyErrorOptions = options.without("-Xfatal-warnings") + + val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A" + val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A" + val classB1 = defaultOutputDir + tastyErrorGroup + "/B/v1/B" + + val tests = List( + compileFile("tests/init/tasty-error/val-or-defdef/v1/A.scala", tastyErrorOptions)(tastyErrorGroup), + compileFile("tests/init/tasty-error/val-or-defdef/v1/B.scala", tastyErrorOptions.withClasspath(classA1))(tastyErrorGroup), + compileFile("tests/init/tasty-error/val-or-defdef/v0/A.scala", tastyErrorOptions)(tastyErrorGroup), + ).map(_.keepOutput.checkCompile()) + + compileFile("tests/init/tasty-error/val-or-defdef/Main.scala", tastyErrorOptions.withClasspath(classA0).withClasspath(classB1))(tastyErrorGroup).checkExpectedErrors() + + tests.foreach(_.delete()) + } + + /* This tests for errors in the program's TASTy trees. + * The test consists of five files: Main, C, v1/A, v1/B, and v0/A. The files v1/A, v1/B, and v0/A all depend on C. v1/A and v1/B are + * compatible, but v1/B and v0/A are not. If v1/B and v0/A are compiled together, there should be + * an error when reading the files' TASTy trees. This fact is demonstrated by the compilation of Main. */ + locally { + val tastyErrorGroup = TestGroup("checkInit/tasty-error/typedef") + val tastyErrorOptions = options.without("-Xfatal-warnings").without("-Ycheck:all") + + val classC = defaultOutputDir + tastyErrorGroup + "/C/typedef/C" + val classA0 = defaultOutputDir + tastyErrorGroup + "/A/v0/A" + val classA1 = defaultOutputDir + tastyErrorGroup + "/A/v1/A" + val classB1 = defaultOutputDir + tastyErrorGroup + "/B/v1/B" + + val tests = List( + compileFile("tests/init/tasty-error/typedef/C.scala", tastyErrorOptions)(tastyErrorGroup), + compileFile("tests/init/tasty-error/typedef/v1/A.scala", tastyErrorOptions.withClasspath(classC))(tastyErrorGroup), + compileFile("tests/init/tasty-error/typedef/v1/B.scala", tastyErrorOptions.withClasspath(classC).withClasspath(classA1))(tastyErrorGroup), + compileFile("tests/init/tasty-error/typedef/v0/A.scala", tastyErrorOptions.withClasspath(classC))(tastyErrorGroup), + ).map(_.keepOutput.checkCompile()) + + compileFile("tests/init/tasty-error/typedef/Main.scala", tastyErrorOptions.withClasspath(classC).withClasspath(classA0).withClasspath(classB1))(tastyErrorGroup).checkExpectedErrors() + + tests.foreach(_.delete()) + } } // parallel backend tests diff --git a/compiler/test/dotty/tools/dotc/FromTastyTests.scala b/compiler/test/dotty/tools/dotc/FromTastyTests.scala index 1d46cbbce95c..874088fb618e 100644 --- a/compiler/test/dotty/tools/dotc/FromTastyTests.scala +++ b/compiler/test/dotty/tools/dotc/FromTastyTests.scala @@ -23,7 +23,7 @@ class FromTastyTests { implicit val testGroup: TestGroup = TestGroup("posTestFromTasty") compileTastyInDir(s"tests${JFile.separator}pos", defaultOptions, - fromTastyFilter = FileFilter.exclude(TestSources.posFromTastyBlacklisted) + fromTastyFilter = FileFilter.exclude(TestSources.posFromTastyExcludelisted) ).checkCompile() } @@ -35,7 +35,7 @@ class FromTastyTests { implicit val testGroup: TestGroup = TestGroup("runTestFromTasty") compileTastyInDir(s"tests${JFile.separator}run", defaultOptions, - fromTastyFilter = FileFilter.exclude(TestSources.runFromTastyBlacklisted) + fromTastyFilter = FileFilter.exclude(TestSources.runFromTastyExcludelisted) ).checkRuns() } } diff --git a/compiler/test/dotty/tools/dotc/config/PropertiesTest.scala b/compiler/test/dotty/tools/dotc/config/PropertiesTest.scala new file mode 100644 index 000000000000..e45ac1f3983f --- /dev/null +++ b/compiler/test/dotty/tools/dotc/config/PropertiesTest.scala @@ -0,0 +1,45 @@ +package dotty.tools.dotc.config + +import org.junit.Before +import org.junit.Test +import org.junit.Assert._ +import scala.language.unsafeNulls + +class PropertiesTest { + final val TestProperty = "dotty.tools.dotc.config.PropertiesTest.__test_property__" + + @Before + def beforeEach(): Unit = { + Properties.clearProp(TestProperty) + } + + @Test + def testPropOrNone(): Unit = { + assertEquals(Properties.propOrNone(TestProperty), None) + + Properties.setProp(TestProperty, "foo") + + assertEquals(Properties.propOrNone(TestProperty), Some("foo")) + } + + @Test + def testPropOrElse(): Unit = { + assertEquals(Properties.propOrElse(TestProperty, "bar"), "bar") + + Properties.setProp(TestProperty, "foo") + + var done = false + assertEquals(Properties.propOrElse(TestProperty, { done = true; "bar" }), "foo") + assertFalse("Does not evaluate alt if not needed", done) + } + + @Test + def testEnvOrElse(): Unit = { + assertEquals(Properties.envOrElse("_PropertiesTest_NOT_DEFINED", "test"), "test") + + var done = false + val envName = System.getenv().keySet().iterator().next() + assertNotEquals(Properties.envOrElse(envName, {done = true; "bar"}), "bar") + assertFalse("Does not evaluate alt if not needed", done) + } +} diff --git a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala index d6ef59e9f00a..69dffdce87ba 100644 --- a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala +++ b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala @@ -199,4 +199,11 @@ class ScalaSettingsTests: ) assertEquals(result, Right(reporting.Action.Error)) + @Test def `illegal source versions are not accepted when parsing the settings`: Unit = + for source <- SourceVersion.illegalInSettings do + val settings = new ScalaSettings + val result = settings.processArguments(List("-source", source.toString()), true) + assertEquals(0, result.warnings.length) + assertEquals(1, result.errors.length) + end ScalaSettingsTests diff --git a/compiler/test/dotty/tools/dotc/parsing/ScannerTest.scala b/compiler/test/dotty/tools/dotc/parsing/ScannerTest.scala index 659cd27e62f4..9f78d0778b41 100644 --- a/compiler/test/dotty/tools/dotc/parsing/ScannerTest.scala +++ b/compiler/test/dotty/tools/dotc/parsing/ScannerTest.scala @@ -10,7 +10,7 @@ import org.junit.Test class ScannerTest extends DottyTest { - val blackList = List( + val excluded = List( "/scaladoc/scala/tools/nsc/doc/html/page/Index.scala", "/scaladoc/scala/tools/nsc/doc/html/page/Template.scala" ) @@ -33,13 +33,13 @@ class ScannerTest extends DottyTest { def scanDir(path: String): Unit = scanDir(Directory(path)) def scanDir(dir: Directory): Unit = { - if (blackList exists (dir.jpath.toString endsWith _)) - println(s"blacklisted package: ${dir.toAbsolute.jpath}") + if (excluded exists (dir.jpath.toString endsWith _)) + println(s"excluded package: ${dir.toAbsolute.jpath}") else for (f <- dir.files) if (f.name.endsWith(".scala")) - if (blackList exists (f.jpath.toString endsWith _)) - println(s"blacklisted file: ${f.toAbsolute.jpath}") + if (excluded exists (f.jpath.toString endsWith _)) + println(s"excluded file: ${f.toAbsolute.jpath}") else scan(new PlainFile(f)) for (d <- dir.dirs) diff --git a/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala b/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala index cfcade9be5ab..0d235bace899 100644 --- a/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala +++ b/compiler/test/dotty/tools/dotc/printing/PrintingTest.scala @@ -27,7 +27,7 @@ class PrintingTest { def options(phase: String, flags: List[String]) = val outDir = ParallelTesting.defaultOutputDir + "printing" + File.pathSeparator File(outDir).mkdirs() - List(s"-Xprint:$phase", "-color:never", "-nowarn", "-d", outDir, "-classpath", TestConfiguration.basicClasspath) ::: flags + List(s"-Vprint:$phase", "-color:never", "-nowarn", "-d", outDir, "-classpath", TestConfiguration.basicClasspath) ::: flags private def compileFile(path: JPath, phase: String): Boolean = { val baseFilePath = path.toString.stripSuffix(".scala") diff --git a/compiler/test/dotty/tools/dotc/semanticdb/SemanticdbTests.scala b/compiler/test/dotty/tools/dotc/semanticdb/SemanticdbTests.scala index de8c2c11f9c2..cdfb3cea5856 100644 --- a/compiler/test/dotty/tools/dotc/semanticdb/SemanticdbTests.scala +++ b/compiler/test/dotty/tools/dotc/semanticdb/SemanticdbTests.scala @@ -138,7 +138,7 @@ class SemanticdbTests: "-feature", "-deprecation", // "-Ydebug-flags", - // "-Xprint:extractSemanticDB", + // "-Vprint:extractSemanticDB", "-sourceroot", expectSrc.toString, "-classpath", target.toString, "-Xignore-scala2-macros", diff --git a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala index 9fe5ffcf072e..1f65f7fa626f 100644 --- a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala +++ b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala @@ -400,6 +400,16 @@ class ReplCompilerTests extends ReplTest: assertTrue(all.head.startsWith("-- [E103] Syntax Error")) assertTrue(all.exists(_.trim().startsWith("| Illegal start of statement: this modifier is not allowed here"))) + @Test def `i22844 regression colon eol`: Unit = initially: + run: + """|println: + | "hello, world" + |""".stripMargin // outdent, but this test does not exercise the bug + assertEquals(List("hello, world"), lines()) + + @Test def `i22844b regression colon arrow eol`: Unit = contextually: + assertTrue(ParseResult.isIncomplete("List(42).map: x =>")) + object ReplCompilerTests: private val pattern = Pattern.compile("\\r[\\n]?|\\n"); @@ -412,7 +422,7 @@ object ReplCompilerTests: end ReplCompilerTests -class ReplXPrintTyperTests extends ReplTest(ReplTest.defaultOptions :+ "-Xprint:typer"): +class ReplXPrintTyperTests extends ReplTest(ReplTest.defaultOptions :+ "-Vprint:typer"): @Test def i9111 = initially { run("""|enum E { | case A diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index 8fbf635c9a17..b630ef0c2b82 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -94,10 +94,7 @@ extends ReplDriver(options, new PrintStream(out, true, StandardCharsets.UTF_8.na FileDiff.dump(checkFile.toPath.toString, actualOutput) println(s"Wrote updated script file to $checkFile") else - println("expected =========>") - println(expectedOutput.mkString(EOL)) - println("actual ===========>") - println(actualOutput.mkString(EOL)) + println(dotc.util.DiffUtil.mkColoredHorizontalLineDiff(actualOutput.mkString(EOL), expectedOutput.mkString(EOL))) fail(s"Error in script $name, expected output did not match actual") end if diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 963d680b69bc..d8518e9f41dc 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -216,10 +216,14 @@ class TabcompleteTests extends ReplTest { ":exit", ":help", ":imports", + ":jar", + ":kind", ":load", ":quit", + ":require", ":reset", ":settings", + ":sh", ":silent", ":type" ), diff --git a/compiler/test/dotty/tools/utils.scala b/compiler/test/dotty/tools/utils.scala index a8c480088e08..e3ee944decf1 100644 --- a/compiler/test/dotty/tools/utils.scala +++ b/compiler/test/dotty/tools/utils.scala @@ -89,6 +89,9 @@ private val toolArg = raw"(?://|/\*| \*) ?(?i:(${ToolName.values.mkString("|")}) /** Directive to specify to vulpix the options to pass to Dotty */ private val directiveOptionsArg = raw"//> using options (.*)".r.unanchored private val directiveJavacOptions = raw"//> using javacOpt (.*)".r.unanchored +private val directiveTargetOptions = raw"//> using target.platform (jvm|scala-js)".r.unanchored +private val directiveUnsupported = raw"//> using (scala) (.*)".r.unanchored +private val directiveUnknown = raw"//> using (.*)".r.unanchored // Inspect the lines for compiler options of the form // `//> using options args`, `// scalajs: args`, `/* scalajs: args`, ` * scalajs: args` etc. @@ -101,10 +104,13 @@ def toolArgsParse(lines: List[String], filename: Option[String]): List[(String,S case toolArg(name, args) => List((name, args)) case _ => Nil } ++ - lines.flatMap { + lines.flatMap { case directiveOptionsArg(args) => List(("scalac", args)) case directiveJavacOptions(args) => List(("javac", args)) - case _ => Nil + case directiveTargetOptions(platform) => List(("target", platform)) + case directiveUnsupported(name, args) => Nil + case directiveUnknown(rest) => sys.error(s"Unknown directive: `//> using ${CommandLineParser.tokenize(rest).headOption.getOrElse("''")}`${filename.fold("")(f => s" in file $f")}") + case _ => Nil } import org.junit.Test diff --git a/compiler/test/dotty/tools/vulpix/FileFilter.scala b/compiler/test/dotty/tools/vulpix/FileFilter.scala index b2aef8af038e..b59b4d4f209d 100644 --- a/compiler/test/dotty/tools/vulpix/FileFilter.scala +++ b/compiler/test/dotty/tools/vulpix/FileFilter.scala @@ -11,13 +11,13 @@ object FileFilter { exclude(file :: files.toList) def exclude(files: List[String]): FileFilter = new FileFilter { - private val blackList = files.toSet - def accept(file: String): Boolean = !blackList.contains(file) + private val excluded = files.toSet + def accept(file: String): Boolean = !excluded.contains(file) } def include(files: List[String]): FileFilter = new FileFilter { - private val whiteList = files.toSet - def accept(file: String): Boolean = whiteList.contains(file) + private val included = files.toSet + def accept(file: String): Boolean = included.contains(file) } object NoFilter extends FileFilter { diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index f701a348d233..08ade9b035eb 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -12,7 +12,7 @@ import java.nio.file.{Files, NoSuchFileException, Path, Paths} import java.nio.charset.{Charset, StandardCharsets} import java.text.SimpleDateFormat import java.util.{HashMap, Timer, TimerTask} -import java.util.concurrent.{ExecutionException, TimeUnit, TimeoutException, Executors => JExecutors} +import java.util.concurrent.{TimeUnit, TimeoutException, Executors => JExecutors} import scala.collection.mutable import scala.io.{Codec, Source} @@ -156,6 +156,11 @@ trait ParallelTesting extends RunnerOrchestration { self => } } } + + final override def toString: String = sourceFiles match { + case Array(f) => f.getPath + case _ => outDir.getPath.stripPrefix(defaultOutputDir).stripPrefix(name).stripPrefix("/") + } } /** A group of files that may all be compiled together, with the same flags @@ -170,8 +175,6 @@ trait ParallelTesting extends RunnerOrchestration { self => decompilation: Boolean = false ) extends TestSource { def sourceFiles: Array[JFile] = files.filter(isSourceFile) - - override def toString() = sourceFiles match { case Array(f) => f.getPath case _ => outDir.getPath } } /** A test source whose files will be compiled separately according to their @@ -508,12 +511,6 @@ trait ParallelTesting extends RunnerOrchestration { self => .and("-d", targetDir.getPath) .withClasspath(targetDir.getPath) - def waitForJudiciously(process: Process): Int = - try process.waitFor() - catch case _: InterruptedException => - try if process.waitFor(5L, TimeUnit.MINUTES) then process.exitValue() else -2 - finally Thread.currentThread.interrupt() - def compileWithJavac(fs: Array[String]) = if (fs.nonEmpty) { val fullArgs = Array( "-encoding", StandardCharsets.UTF_8.name, @@ -522,7 +519,7 @@ trait ParallelTesting extends RunnerOrchestration { self => val process = Runtime.getRuntime.exec("javac" +: fullArgs) val output = Source.fromInputStream(process.getErrorStream).mkString - if waitForJudiciously(process) != 0 then Some(output) + if process.waitFor() != 0 then Some(output) else None } else None @@ -701,11 +698,7 @@ trait ParallelTesting extends RunnerOrchestration { self => for fut <- eventualResults do try fut.get() - catch - case ee: ExecutionException if ee.getCause.isInstanceOf[InterruptedException] => - System.err.println("Interrupted (probably running after shutdown)") - ee.printStackTrace() - case ex: Exception => + catch case ex: Exception => System.err.println(ex.getMessage) ex.printStackTrace() @@ -743,13 +736,14 @@ trait ParallelTesting extends RunnerOrchestration { self => diffCheckfile(testSource, reporters, logger) override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] = - lazy val (map, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq) + lazy val (expected, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq) lazy val obtCount = reporters.foldLeft(0)(_ + _.warningCount) - lazy val (expected, unexpected) = getMissingExpectedWarnings(map, reporters.iterator.flatMap(_.diagnostics)) - lazy val diagnostics = reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line).map(e => s" at ${e.pos.line + 1}: ${e.message}")) - def showLines(title: String, lines: Seq[String]) = if lines.isEmpty then "" else title + lines.mkString("\n", "\n", "") - def hasMissingAnnotations = expected.nonEmpty || unexpected.nonEmpty - def showDiagnostics = showLines("-> following the diagnostics:", diagnostics) + lazy val (unfulfilled, unexpected) = getMissingExpectedWarnings(expected, diagnostics.iterator) + lazy val diagnostics = reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line)) + lazy val messages = diagnostics.map(d => s" at ${d.pos.line + 1}: ${d.message}") + def showLines(title: String, lines: Seq[String]) = if lines.isEmpty then "" else lines.mkString(s"$title\n", "\n", "") + def hasMissingAnnotations = unfulfilled.nonEmpty || unexpected.nonEmpty + def showDiagnostics = showLines("-> following the diagnostics:", messages) Option: if reporters.exists(_.errorCount > 0) then s"""Compilation failed for: ${testSource.title} @@ -758,58 +752,63 @@ trait ParallelTesting extends RunnerOrchestration { self => else if expCount != obtCount then s"""|Wrong number of warnings encountered when compiling $testSource |expected: $expCount, actual: $obtCount - |${showLines("Unfulfilled expectations:", expected)} + |${showLines("Unfulfilled expectations:", unfulfilled)} |${showLines("Unexpected warnings:", unexpected)} |$showDiagnostics |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "") - else if hasMissingAnnotations then s"\nWarnings found on incorrect row numbers when compiling $testSource\n$showDiagnostics" - else if !map.isEmpty then s"\nExpected warnings(s) have {=}: $map" + else if hasMissingAnnotations then + s"""|Warnings found on incorrect row numbers when compiling $testSource + |${showLines("Unfulfilled expectations:", unfulfilled)} + |${showLines("Unexpected warnings:", unexpected)} + |$showDiagnostics + |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "") + else if !expected.isEmpty then s"\nExpected warnings(s) have {=}: $expected" else null end maybeFailureMessage def getWarnMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) = - val comment = raw"//( *)(nopos-)?warn".r - val map = new HashMap[String, Integer]() + val comment = raw"//(?: *)(nopos-)?warn".r + val map = HashMap[String, Integer]() var count = 0 def bump(key: String): Unit = map.get(key) match case null => map.put(key, 1) case n => map.put(key, n+1) count += 1 - files.filter(isSourceFile).foreach { file => - Using(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source => - source.getLines.zipWithIndex.foreach { case (line, lineNbr) => - comment.findAllMatchIn(line).foreach { m => - m.group(2) match - case "nopos-" => - bump("nopos") - case _ => bump(s"${file.getPath}:${lineNbr+1}") - } - } - }.get - } + for file <- files if isSourceFile(file) do + Using.resource(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source => + source.getLines.zipWithIndex.foreach: (line, lineNbr) => + comment.findAllMatchIn(line).foreach: + case comment("nopos-") => bump("nopos") + case _ => bump(s"${file.getPath}:${lineNbr+1}") + } + end for (map, count) - def getMissingExpectedWarnings(map: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) = - val unexpected, unpositioned = ListBuffer.empty[String] + // return unfulfilled expected warnings and unexpected diagnostics + def getMissingExpectedWarnings(expected: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) = + val unexpected = ListBuffer.empty[String] def relativize(path: String): String = path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator) def seenAt(key: String): Boolean = - map.get(key) match + expected.get(key) match case null => false - case 1 => map.remove(key) ; true - case n => map.put(key, n - 1) ; true + case 1 => expected.remove(key); true + case n => expected.put(key, n - 1); true def sawDiagnostic(d: Diagnostic): Unit = val srcpos = d.pos.nonInlined if srcpos.exists then val key = s"${relativize(srcpos.source.file.toString())}:${srcpos.line + 1}" if !seenAt(key) then unexpected += key else - if(!seenAt("nopos")) unpositioned += relativize(srcpos.source.file.toString()) + if !seenAt("nopos") then unexpected += relativize(srcpos.source.file.toString) reporterWarnings.foreach(sawDiagnostic) - (map.asScala.keys.toList, (unexpected ++ unpositioned).toList) + val splitter = raw"(?:[^:]*):(\d+)".r + val unfulfilled = expected.asScala.keys.toList.sortBy { case splitter(n) => n.toInt case _ => -1 } + (unfulfilled, unexpected.toList) end getMissingExpectedWarnings + end WarnTest private final class RewriteTest(testSources: List[TestSource], checkFiles: Map[JFile, JFile], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) extends Test(testSources, times, threadLimit, suppressAllOutput) { @@ -944,8 +943,8 @@ trait ParallelTesting extends RunnerOrchestration { self => def seenAt(key: String): Boolean = errorMap.get(key) match case null => false - case 1 => errorMap.remove(key) ; true - case n => errorMap.put(key, n - 1) ; true + case 1 => errorMap.remove(key); true + case n => errorMap.put(key, n - 1); true def sawDiagnostic(d: Diagnostic): Unit = d.pos.nonInlined match case srcpos if srcpos.exists => @@ -1499,7 +1498,7 @@ trait ParallelTesting extends RunnerOrchestration { self => | | sbt "testCompilation --from-tasty $file" | - |This tests can be disabled by adding `${file.getName}` to `compiler${JFile.separator}test${JFile.separator}dotc${JFile.separator}$runOrPos-$listName.blacklist` + |This tests can be disabled by adding `${file.getName}` to `compiler${JFile.separator}test${JFile.separator}dotc${JFile.separator}$runOrPos-$listName.excludelist` | |""".stripMargin } diff --git a/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala b/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala index baf61c845d96..ab8a611caa33 100644 --- a/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala +++ b/compiler/test/dotty/tools/vulpix/VulpixUnitTests.scala @@ -105,7 +105,7 @@ object VulpixUnitTests extends ParallelTesting { def maxDuration = 3.seconds def numberOfSlaves = 5 def safeMode = sys.env.get("SAFEMODE").isDefined - def isInteractive = !sys.env.contains("DRONE") + def isInteractive = !sys.env.contains("DOTTY_CI_RUN") def testFilter = Nil def updateCheckFiles: Boolean = false def failedTests = None diff --git a/docs/_docs/contributing/debugging/ide-debugging.md b/docs/_docs/contributing/debugging/ide-debugging.md index af817826565a..cca5d4849e5e 100644 --- a/docs/_docs/contributing/debugging/ide-debugging.md +++ b/docs/_docs/contributing/debugging/ide-debugging.md @@ -140,7 +140,7 @@ And concatenate the output into the classpath argument, which should already con In the `args` you can add any additional compiler option you want. -For instance you can add `-Xprint:all` to print all the generated trees after each mega phase. +For instance you can add `-Vprint:all` to print all the generated trees after each mega phase. Run `scalac -help` to get an overview of the available compiler options. diff --git a/docs/_docs/contributing/debugging/inspection.md b/docs/_docs/contributing/debugging/inspection.md index a80c3d3462ae..483f9db4474e 100644 --- a/docs/_docs/contributing/debugging/inspection.md +++ b/docs/_docs/contributing/debugging/inspection.md @@ -61,9 +61,9 @@ Sometimes you may want to stop the compiler after a certain phase, for example t knock-on errors from occurring from a bug in an earlier phase. Use the flag `-Ystop-after:` to prevent any phases executing afterwards. -> e.g. `-Xprint:` where `phase` is a miniphase, will print after +> e.g. `-Vprint:` where `phase` is a miniphase, will print after > the whole phase group is complete, which may be several miniphases after `phase`. -> Instead you can use `-Ystop-after: -Xprint:` to stop +> Instead you can use `-Ystop-after: -Vprint:` to stop > immediately after the miniphase and see the trees that you intended. ## Printing TASTy of a Class diff --git a/docs/_docs/contributing/debugging/other-debugging.md b/docs/_docs/contributing/debugging/other-debugging.md index 50be43db51ab..3879f3d8b53d 100644 --- a/docs/_docs/contributing/debugging/other-debugging.md +++ b/docs/_docs/contributing/debugging/other-debugging.md @@ -137,19 +137,19 @@ assertPositioned(tree.reporting(s"Tree is: $result")) To print out the trees you are compiling after the FrontEnd (scanner, parser, namer, typer) phases: ```shell -scalac -Xprint:typer ../issues/Playground.scala +scalac -Vprint:typer ../issues/Playground.scala ``` To print out the trees after Frontend and CollectSuperCalls phases: ```shell -scalac -Xprint:typer,collectSuperCalls ../issues/Playground.scala +scalac -Vprint:typer,collectSuperCalls ../issues/Playground.scala ``` To print out the trees after all phases: ```shell -scalac -Xprint:all ../issues/Playground.scala +scalac -Vprint:all ../issues/Playground.scala ``` To find out the list of all the phases and their names, check out [this](https://github.com/lampepfl/dotty/blob/10526a7d0aa8910729b6036ee51942e05b71abf6/compiler/src/dotty/tools/dotc/Compiler.scala#L34) line in `Compiler.scala`. Each `Phase` object has `phaseName` defined on it, this is the phase name. @@ -219,7 +219,7 @@ And is to be used as: scalac -Yprint-pos ../issues/Playground.scala ``` -If used, all the trees output with `show` or via `-Xprint:typer` will also have positions attached to them, e.g.: +If used, all the trees output with `show` or via `-Vprint:typer` will also have positions attached to them, e.g.: ```scala package @ { @@ -247,7 +247,7 @@ package @ { Every [Positioned](https://github.com/lampepfl/dotty/blob/10526a7d0aa8910729b6036ee51942e05b71abf6/compiler/src/dotty/tools/dotc/ast/Positioned.scala) (a parent class of `Tree`) object has a `uniqueId` field. It is an integer that is unique for that tree and doesn't change from compile run to compile run. You can output these IDs from any printer (such as the ones used by `.show` and `-Xprint`) via `-Yshow-tree-ids` flag, e.g.: ```shell -scalac -Xprint:typer -Yshow-tree-ids ../issues/Playground.scala +scalac -Vprint:typer -Yshow-tree-ids ../issues/Playground.scala ``` Gives: diff --git a/docs/_docs/contributing/issues/cause.md b/docs/_docs/contributing/issues/cause.md index e23f6d1f747f..b6bbb68a052f 100644 --- a/docs/_docs/contributing/issues/cause.md +++ b/docs/_docs/contributing/issues/cause.md @@ -18,10 +18,10 @@ As described in the [compiler lifecycle](../architecture/lifecycle.md#phases-2), each phase transforms the trees and types that represent your code in a certain way. -To print the code as it is transformed through the compiler, use the compiler flag `-Xprint:all`. +To print the code as it is transformed through the compiler, use the compiler flag `-Vprint:all`. After each phase group is completed, you will see the resulting trees representing the code. -> It is recommended to test `-Xprint:all` on a single, small file, otherwise a lot of unnecessary +> It is recommended to test `-Vprint:all` on a single, small file, otherwise a lot of unnecessary > output will be generated. ### Trace a Tree Creation Site @@ -31,7 +31,7 @@ your search to the code of that phase. For example if you found a problematic tr `posttyper`, the problem most likely appears in the code of [PostTyper]. We can trace the exact point the tree was generated by looking for its unique ID, and then generating a stack trace at its creation: -1. Run the compiler with `-Xprint:posttyper` and `-Yshow-tree-ids` flags. +1. Run the compiler with `-Vprint:posttyper` and `-Yshow-tree-ids` flags. This will only print the trees of the `posttyper` phase. This time you should see the tree in question be printed alongside its ID. You'll see something like `println#223("Hello World"#37)`. 2. Copy the ID of the desired tree. @@ -43,7 +43,7 @@ Do not use a conditional breakpoint, the time overhead is very significant for a ### Enhanced Tree Printing -As seen above `-Xprint:` can be enhanced with further configuration flags, found in +As seen above `-Vprint:` can be enhanced with further configuration flags, found in [ScalaSettings]. For example, you can additionally print the type of a tree with `-Xprint-types`. ## Increasing Logging Output diff --git a/docs/_docs/contributing/issues/reproduce.md b/docs/_docs/contributing/issues/reproduce.md index ca5da324a867..cb26e7eb764e 100644 --- a/docs/_docs/contributing/issues/reproduce.md +++ b/docs/_docs/contributing/issues/reproduce.md @@ -39,9 +39,9 @@ $ scalac Here are some useful debugging ``: -* `-Xprint:PHASE1,PHASE2,...` or `-Xprint:all`: prints the `AST` after each +* `-Vprint:PHASE1,PHASE2,...` or `-Vprint:all`: prints the `AST` after each specified phase. Phase names can be found by examining the - `dotty.tools.dotc.transform.*` classes for their `phaseName` field e.g., `-Xprint:erasure`. + `dotty.tools.dotc.transform.*` classes for their `phaseName` field e.g., `-Vprint:erasure`. You can discover all phases in the `dotty.tools.dotc.Compiler` class * `-Ylog:PHASE1,PHASE2,...` or `-Ylog:all`: enables `ctx.log("")` logging for the specified phase. @@ -142,7 +142,7 @@ $ (rm -rv out || true) && mkdir out # clean up compiler output, create `out` dir scalac # Invoke the compiler task defined by the Dotty sbt project -d $here/out # All the artefacts go to the `out` folder created earlier - # -Xprint:typer # Useful debug flags, commented out and ready for quick usage. Should you need one, you can quickly access it by uncommenting it. + # -Vprint:typer # Useful debug flags, commented out and ready for quick usage. Should you need one, you can quickly access it by uncommenting it. # -Ydebug-error # -Yprint-debug # -Yprint-debug-owners diff --git a/docs/_docs/contributing/setting-up-your-ide.md b/docs/_docs/contributing/setting-up-your-ide.md index 3779ce1c3403..690b42a37a51 100644 --- a/docs/_docs/contributing/setting-up-your-ide.md +++ b/docs/_docs/contributing/setting-up-your-ide.md @@ -42,7 +42,15 @@ want to make sure you do two things: + val enableBspAllProjects = true, ``` -2. Run `sbt publishLocal` to get the needed presentation compiler jars. +2. Run in sbt shell `sbt> scala3-bootstrapped/compile` and then `sbt> scala3-bootstrapped/publishLocalBin` + to get the required presentation compiler jars. + + If any step fails due to random errors, try removing `./out/` directory and running `sbt> clean` + + This step has to be repeated every time compiler version has been bumped. + + + By default Metals uses Bloop build server, however you can also use sbt directly. You can achieve this with the `Metals: Switch Build Server` command diff --git a/docs/_docs/reference/dropped-features/this-qualifier.md b/docs/_docs/reference/dropped-features/this-qualifier.md index f75d19356696..541c91e5cdfa 100644 --- a/docs/_docs/reference/dropped-features/this-qualifier.md +++ b/docs/_docs/reference/dropped-features/this-qualifier.md @@ -29,3 +29,15 @@ This can cause problems if a program tries to access the missing private field v // [C] needed if `field` is to be accessed through reflection val retained = field * field ``` + +Class parameters are normally inferred object-private, +so that members introduced by explicitly declaring them `val` or `var` are exempt from the rule described here. + +In particular, the following field is not excluded from variance checking: +```scala + class C[-T](private val t: T) // error +``` +And in contrast to the private field shown above, this field is not eliminated: +```scala + class C(private val c: Int) +``` diff --git a/docs/_docs/reference/dropped-features/type-projection.md b/docs/_docs/reference/dropped-features/type-projection.md index 08b5ffb34eca..9b9f643ceb6e 100644 --- a/docs/_docs/reference/dropped-features/type-projection.md +++ b/docs/_docs/reference/dropped-features/type-projection.md @@ -4,15 +4,18 @@ title: "Dropped: General Type Projection" nightlyOf: https://docs.scala-lang.org/scala3/reference/dropped-features/type-projection.html --- -Scala so far allowed general type projection `T#A` where `T` is an arbitrary type -and `A` names a type member of `T`. +Scala 2 allowed general type projection `T#A` where `T` is an arbitrary type and `A` names a type member of `T`. +This turns out to be [unsound](https://github.com/scala/scala3/issues/1050) (at least when combined with other Scala 3 features). -Scala 3 disallows this if `T` is an abstract type (class types and type aliases -are fine). This change was made because unrestricted type projection -is [unsound](https://github.com/lampepfl/dotty/issues/1050). - -This restriction rules out the [type-level encoding of a combinator -calculus](https://michid.wordpress.com/2010/01/29/scala-type-level-encoding-of-the-ski-calculus/). +To remedy this, Scala 3 only allows type projection if `T` is a concrete type (any type which is not abstract), an example for such a type would be a class type (`class T`). +A type is abstract if it is: +* An abstract type member (`type T` without `= SomeType`) +* A type parameter (`[T]`) +* An alias to an abstract type (`type T = SomeAbstractType`). +There are no restriction on `A` apart from the fact it has to be a member type of `T`, for example a subclass (`class T { class A }`). To rewrite code using type projections on abstract types, consider using path-dependent types or implicit parameters. + +This restriction rules out the [type-level encoding of a combinator +calculus](https://michid.wordpress.com/2010/01/29/scala-type-level-encoding-of-the-ski-calculus/). \ No newline at end of file diff --git a/docs/_docs/reference/experimental/cc.md b/docs/_docs/reference/experimental/cc.md index 162a4661e10c..ff7c01704b78 100644 --- a/docs/_docs/reference/experimental/cc.md +++ b/docs/_docs/reference/experimental/cc.md @@ -656,7 +656,7 @@ TBD The following options are relevant for capture checking. - - **-Xprint:cc** Prints the program with capturing types as inferred by capture checking. + - **-Vprint:cc** Prints the program with capturing types as inferred by capture checking. - **-Ycc-debug** Gives more detailed, implementation-oriented information about capture checking, as described in the next section. The implementation supporting capture checking with these options is currently in branch `cc-experiment` on dotty.epfl.ch. diff --git a/docs/_docs/reference/other-new-features/creator-applications.md b/docs/_docs/reference/other-new-features/creator-applications.md index 8b1de02b2f25..f3b58b87bc48 100644 --- a/docs/_docs/reference/other-new-features/creator-applications.md +++ b/docs/_docs/reference/other-new-features/creator-applications.md @@ -39,8 +39,11 @@ The precise rules are as follows: - the class has a companion object (which might have been generated in step 1), and - that companion object does not already define a member named `apply`. - Each generated `apply` method forwards to one constructor of the class. It has the - same type and value parameters as the constructor. + Each generated `apply` method forwards to one constructor of the class. + It has the same type and value parameters as the constructor, + as well as the same access restriction as the class. + If the class is `protected`, then either the companion object must be `protected` + or the `apply` method will be made `protected`. Constructor proxy companions cannot be used as values by themselves. A proxy companion object must be selected with `apply` (or be applied to arguments, in which case the `apply` is implicitly diff --git a/docs/_docs/reference/other-new-features/indentation.md b/docs/_docs/reference/other-new-features/indentation.md index 9963d1ee7577..103de5d6797a 100644 --- a/docs/_docs/reference/other-new-features/indentation.md +++ b/docs/_docs/reference/other-new-features/indentation.md @@ -189,7 +189,7 @@ Define for an arbitrary sequence of tokens or non-terminals `TS`: ```ebnf :<<< TS >>> ::= ‘{’ TS ‘}’ - | + | TS ``` Then the grammar changes as follows: ```ebnf diff --git a/docs/_docs/reference/other-new-features/type-test.md b/docs/_docs/reference/other-new-features/type-test.md index ec7a87230753..fb2a2e584711 100644 --- a/docs/_docs/reference/other-new-features/type-test.md +++ b/docs/_docs/reference/other-new-features/type-test.md @@ -63,7 +63,7 @@ We could create a type test at call site where the type test can be performed wi val tt: TypeTest[Any, String] = new TypeTest[Any, String]: def unapply(s: Any): Option[s.type & String] = s match - case s: String => Some(s) + case q: (s.type & String) => Some(q) case _ => None f[AnyRef, String]("acb")(using tt) diff --git a/docs/_spec/03-types.md b/docs/_spec/03-types.md index 4b1293258495..5d4e205aace9 100644 --- a/docs/_spec/03-types.md +++ b/docs/_spec/03-types.md @@ -1071,7 +1071,7 @@ The following properties hold about ´⌈X⌉´ (we have paper proofs for those) The "lower-bound rule" states that ´S <: T´ if ´T = q.X´ and ´q.X´ is a non-class type designator and ´S <: L´ where ´L´ is the lower bound of the underlying type definition of ´q.X´". That rule is known to break transitivy of subtyping in Scala already. -Second, we define the relation ´⋔´ on *classes* (including traits and hidden classes of objects) as: +Second, we define the relation ´⋔´ on *classes* (including traits, hidden classes of objects, and enum terms) as: - ´C ⋔ D´ if `´C ∉´ baseClasses´(D)´` and ´D´ is `final` - ´C ⋔ D´ if `´D ∉´ baseClasses´(C)´` and ´C´ is `final` diff --git a/docs/_spec/TODOreference/experimental/cc.md b/docs/_spec/TODOreference/experimental/cc.md index 878bc0a64ed6..c2011fbcbc88 100644 --- a/docs/_spec/TODOreference/experimental/cc.md +++ b/docs/_spec/TODOreference/experimental/cc.md @@ -655,7 +655,7 @@ TBD The following options are relevant for capture checking. - **-Ycc** Enables capture checking. - - **-Xprint:cc** Prints the program with capturing types as inferred by capture checking. + - **-Vprint:cc** Prints the program with capturing types as inferred by capture checking. - **-Ycc-debug** Gives more detailed, implementation-oriented information about capture checking, as described in the next section. The implementation supporting capture checking with these options is currently in branch `cc-experiment` on dotty.epfl.ch. diff --git a/docs/_spec/TODOreference/other-new-features/type-test.md b/docs/_spec/TODOreference/other-new-features/type-test.md index ec7a87230753..fb2a2e584711 100644 --- a/docs/_spec/TODOreference/other-new-features/type-test.md +++ b/docs/_spec/TODOreference/other-new-features/type-test.md @@ -63,7 +63,7 @@ We could create a type test at call site where the type test can be performed wi val tt: TypeTest[Any, String] = new TypeTest[Any, String]: def unapply(s: Any): Option[s.type & String] = s match - case s: String => Some(s) + case q: (s.type & String) => Some(q) case _ => None f[AnyRef, String]("acb")(using tt) diff --git a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala index ddacd0c868e0..d6de3d971e2b 100644 --- a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala +++ b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala @@ -378,4 +378,38 @@ class DefinitionTest { .definition(m3 to m4, Nil) .definition(m5 to m6, Nil) .definition(m7 to m8, Nil) + + @Test def typeParam: Unit = { + code"""|class Foo[${m1}T${m2}]: + | def test: ${m3}T${m4}""" + .definition(m3 to m4, List(m1 to m2)) + } + + @Test def enumTypeParam: Unit = { + code"""|enum Test[${m1}T${m2}]: + | case EnumCase(value: ${m3}T${m4})""" + .definition(m3 to m4, List(m1 to m2)) + } + + @Test def extMethodTypeParam: Unit = { + code"""extension [${m1}T${m2}](string: String) def xxxx(y: ${m3}T${m4}) = ???""" + .definition(m3 to m4, List(m1 to m2)) + } + + @Test def typeParamCovariant: Unit = { + code"""|class Foo[+${m1}T${m2}]: + | def test: ${m3}T${m4}""" + .definition(m3 to m4, List(m1 to m2)) + } + + @Test def enumTypeParamCovariant: Unit = { + code"""|enum Test[+${m1}T${m2}]: + | case EnumCase(value: ${m3}T${m4})""" + .definition(m3 to m4, List(m1 to m2)) + } + + @Test def extMethodTypeParamCovariant: Unit = { + code"""extension [+${m1}T${m2}](string: String) def xxxx(y: ${m3}T${m4}) = ???""" + .definition(m3 to m4, List(m1 to m2)) + } } diff --git a/library/src/scala/deriving/Mirror.scala b/library/src/scala/deriving/Mirror.scala index 57453a516567..a7477cf0fb2d 100644 --- a/library/src/scala/deriving/Mirror.scala +++ b/library/src/scala/deriving/Mirror.scala @@ -52,7 +52,7 @@ object Mirror { extension [T](p: ProductOf[T]) /** Create a new instance of type `T` with elements taken from product `a`. */ - def fromProductTyped[A <: scala.Product, Elems <: p.MirroredElemTypes](a: A)(using m: ProductOf[A] { type MirroredElemTypes = Elems }): T = + def fromProductTyped[A <: scala.Product, Elems <: p.MirroredElemTypes](a: A)(using ProductOf[A] { type MirroredElemTypes = Elems }): T = p.fromProduct(a) /** Create a new instance of type `T` with elements taken from tuple `t`. */ diff --git a/library/src/scala/quoted/Quotes.scala b/library/src/scala/quoted/Quotes.scala index 873d5e1e993e..70d09b7e4841 100644 --- a/library/src/scala/quoted/Quotes.scala +++ b/library/src/scala/quoted/Quotes.scala @@ -1,7 +1,6 @@ package scala.quoted -import scala.annotation.experimental -import scala.annotation.implicitNotFound +import scala.annotation.{experimental, implicitNotFound, unused} import scala.reflect.TypeTest /** Current Quotes in scope @@ -879,10 +878,14 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => * If `sym` refers to a class member `foo` in class `C`, * returns a tree representing `C.this.foo`. * + * If `sym` refers to an object member `foo` in object C, itself in prefix + * `pre` (which might include `.this`, if it contains a class), + * returns `pre.C.foo`. + * * If `sym` refers to a local definition `foo`, returns * a tree representing `foo`. * - * @note In both cases, the constructed tree should only + * @note In all cases, the constructed tree should only * be spliced into the places where such accesses make sense. * For example, it is incorrect to have `C.this.foo` outside * the class body of `C`, or have `foo` outside the lexical @@ -3553,7 +3556,10 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => /** Methods of the module object `val StringConstant` */ trait StringConstantModule { this: StringConstant.type => - /** Create a constant String value */ + /** Create a constant String value + * + * @throw `IllegalArgumentException` if the argument is `null` + */ def apply(x: String): StringConstant /** Match String value constant and extract its value */ def unapply(constant: StringConstant): Some[String] @@ -4797,7 +4803,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => foldTree(foldTree(foldTree(x, cond)(owner), thenp)(owner), elsep)(owner) case While(cond, body) => foldTree(foldTree(x, cond)(owner), body)(owner) - case Closure(meth, tpt) => + case Closure(meth, _) => foldTree(x, meth)(owner) case Match(selector, cases) => foldTrees(foldTree(x, selector)(owner), cases)(owner) @@ -4875,7 +4881,7 @@ trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching => def traverseTree(tree: Tree)(owner: Symbol): Unit = traverseTreeChildren(tree)(owner) - def foldTree(x: Unit, tree: Tree)(owner: Symbol): Unit = traverseTree(tree)(owner) + def foldTree(@unused x: Unit, tree: Tree)(owner: Symbol): Unit = traverseTree(tree)(owner) protected def traverseTreeChildren(tree: Tree)(owner: Symbol): Unit = foldOverTree((), tree)(owner) diff --git a/library/src/scala/runtime/Arrays.scala b/library/src/scala/runtime/Arrays.scala index 2d98caea4df8..97ff589c6610 100644 --- a/library/src/scala/runtime/Arrays.scala +++ b/library/src/scala/runtime/Arrays.scala @@ -1,5 +1,6 @@ package scala.runtime +import scala.annotation.unused import scala.reflect.ClassTag import java.lang.{reflect => jlr} @@ -26,6 +27,6 @@ object Arrays { /** Create an array of a reference type T. */ - def newArray[Arr](componentType: Class[_], returnType: Class[Arr], dimensions: Array[Int]): Arr = + def newArray[Arr](componentType: Class[_], @unused returnType: Class[Arr], dimensions: Array[Int]): Arr = jlr.Array.newInstance(componentType, dimensions: _*).asInstanceOf[Arr] } diff --git a/library/src/scala/runtime/LazyVals.scala b/library/src/scala/runtime/LazyVals.scala index 33ccea4325a5..f3225a4acbd6 100644 --- a/library/src/scala/runtime/LazyVals.scala +++ b/library/src/scala/runtime/LazyVals.scala @@ -96,13 +96,13 @@ object LazyVals { println(s"CAS($t, $offset, $e, $v, $ord)") val mask = ~(LAZY_VAL_MASK << ord * BITS_PER_LAZY_VAL) val n = (e & mask) | (v.toLong << (ord * BITS_PER_LAZY_VAL)) - unsafe.compareAndSwapLong(t, offset, e, n) + unsafe.compareAndSwapLong(t, offset, e, n): @nowarn("cat=deprecation") } def objCAS(t: Object, offset: Long, exp: Object, n: Object): Boolean = { if (debug) println(s"objCAS($t, $exp, $n)") - unsafe.compareAndSwapObject(t, offset, exp, n) + unsafe.compareAndSwapObject(t, offset, exp, n): @nowarn("cat=deprecation") } def setFlag(t: Object, offset: Long, v: Int, ord: Int): Unit = { @@ -147,7 +147,7 @@ object LazyVals { def get(t: Object, off: Long): Long = { if (debug) println(s"get($t, $off)") - unsafe.getLongVolatile(t, off) + unsafe.getLongVolatile(t, off): @nowarn("cat=deprecation") } // kept for backward compatibility diff --git a/presentation-compiler-testcases/src/tests/macros/metals7460.scala b/presentation-compiler-testcases/src/tests/macros/metals7460.scala new file mode 100644 index 000000000000..0b9b1ca494b2 --- /dev/null +++ b/presentation-compiler-testcases/src/tests/macros/metals7460.scala @@ -0,0 +1,20 @@ +package tests.macros + +import scala.quoted.* + +object Macros7460 { + + transparent inline def foo: String = + ${ fooImpl } + + private def fooImpl(using Quotes): Expr[String] = + Expr("foo...") + + transparent inline def bar: String = + ${ barImpl } + + private def barImpl(using Quotes): Expr[String] = + quotes.reflect.Position.ofMacroExpansion.sourceFile.getJPath.get // this line is the culprit + Expr("bar...") + +} diff --git a/presentation-compiler/src/main/dotty/tools/pc/ApplyArgsExtractor.scala b/presentation-compiler/src/main/dotty/tools/pc/ApplyArgsExtractor.scala new file mode 100644 index 000000000000..9384a0b43e8b --- /dev/null +++ b/presentation-compiler/src/main/dotty/tools/pc/ApplyArgsExtractor.scala @@ -0,0 +1,269 @@ +package dotty.tools.pc + +import scala.util.Try + +import dotty.tools.dotc.ast.Trees.ValDef +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.Flags.Method +import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.core.StdNames.* +import dotty.tools.dotc.core.SymDenotations.NoDenotation +import dotty.tools.dotc.core.Symbols.defn +import dotty.tools.dotc.core.Symbols.NoSymbol +import dotty.tools.dotc.core.Symbols.Symbol +import dotty.tools.dotc.core.Types.* +import dotty.tools.pc.IndexedContext +import dotty.tools.pc.utils.InteractiveEnrichments.* +import scala.annotation.tailrec +import dotty.tools.dotc.core.Denotations.SingleDenotation +import dotty.tools.dotc.core.Denotations.MultiDenotation +import dotty.tools.dotc.util.Spans.Span + +object ApplyExtractor: + def unapply(path: List[Tree])(using Context): Option[Apply] = + path match + case ValDef(_, _, _) :: Block(_, app: Apply) :: _ + if !app.fun.isInfix => Some(app) + case rest => + def getApplyForContextFunctionParam(path: List[Tree]): Option[Apply] = + path match + // fun(arg@@) + case (app: Apply) :: _ => Some(app) + // fun(arg@@), where fun(argn: Context ?=> SomeType) + // recursively matched for multiple context arguments, e.g. Context1 ?=> Context2 ?=> SomeType + case (_: DefDef) :: Block(List(_), _: Closure) :: rest => + getApplyForContextFunctionParam(rest) + case _ => None + for + app <- getApplyForContextFunctionParam(rest) + if !app.fun.isInfix + yield app + end match + + +object ApplyArgsExtractor: + def getArgsAndParams( + optIndexedContext: Option[IndexedContext], + apply: Apply, + span: Span + )(using Context): List[(List[Tree], List[ParamSymbol])] = + def collectArgss(a: Apply): List[List[Tree]] = + def stripContextFuntionArgument(argument: Tree): List[Tree] = + argument match + case Block(List(d: DefDef), _: Closure) => + d.rhs match + case app: Apply => + app.args + case b @ Block(List(_: DefDef), _: Closure) => + stripContextFuntionArgument(b) + case _ => Nil + case v => List(v) + + val args = a.args.flatMap(stripContextFuntionArgument) + a.fun match + case app: Apply => collectArgss(app) :+ args + case _ => List(args) + end collectArgss + + val method = apply.fun + + val argss = collectArgss(apply) + + def fallbackFindApply(sym: Symbol) = + sym.info.member(nme.apply) match + case NoDenotation => Nil + case den => List(den.symbol) + + // fallback for when multiple overloaded methods match the supplied args + def fallbackFindMatchingMethods() = + def matchingMethodsSymbols( + indexedContext: IndexedContext, + method: Tree + ): List[Symbol] = + method match + case Ident(name) => indexedContext.findSymbol(name).getOrElse(Nil) + case Select(This(_), name) => indexedContext.findSymbol(name).getOrElse(Nil) + case sel @ Select(from, name) => + val symbol = from.symbol + val ownerSymbol = + if symbol.is(Method) && symbol.owner.isClass then + Some(symbol.owner) + else Try(symbol.info.classSymbol).toOption + ownerSymbol.map(sym => sym.info.member(name)).collect{ + case single: SingleDenotation => List(single.symbol) + case multi: MultiDenotation => multi.allSymbols + }.getOrElse(Nil) + case Apply(fun, _) => matchingMethodsSymbols(indexedContext, fun) + case _ => Nil + val matchingMethods = + for + indexedContext <- optIndexedContext.toList + potentialMatch <- matchingMethodsSymbols(indexedContext, method) + if potentialMatch.is(Flags.Method) && + potentialMatch.vparamss.length >= argss.length && + Try(potentialMatch.isAccessibleFrom(apply.symbol.info)).toOption + .getOrElse(false) && + potentialMatch.vparamss + .zip(argss) + .reverse + .zipWithIndex + .forall { case (pair, index) => + FuzzyArgMatcher(potentialMatch.tparams) + .doMatch(allArgsProvided = index != 0, span) + .tupled(pair) + } + yield potentialMatch + matchingMethods + end fallbackFindMatchingMethods + + val matchingMethods: List[Symbol] = + if method.symbol.paramSymss.nonEmpty then + val allArgsAreSupplied = + val vparamss = method.symbol.vparamss + vparamss.length == argss.length && vparamss + .zip(argss) + .lastOption + .exists { case (baseParams, baseArgs) => + baseArgs.length == baseParams.length + } + // ``` + // m(arg : Int) + // m(arg : Int, anotherArg : Int) + // m(a@@) + // ``` + // complier will choose the first `m`, so we need to manually look for the other one + if allArgsAreSupplied then + val foundPotential = fallbackFindMatchingMethods() + if foundPotential.contains(method.symbol) then foundPotential + else method.symbol :: foundPotential + else List(method.symbol) + else if method.symbol.is(Method) || method.symbol == NoSymbol then + fallbackFindMatchingMethods() + else fallbackFindApply(method.symbol) + end if + end matchingMethods + + matchingMethods.map { methodSym => + val vparamss = methodSym.vparamss + + // get params and args we are interested in + // e.g. + // in the following case, the interesting args and params are + // - params: [apple, banana] + // - args: [apple, b] + // ``` + // def curry(x: Int)(apple: String, banana: String) = ??? + // curry(1)(apple = "test", b@@) + // ``` + val (baseParams0, baseArgs) = + vparamss.zip(argss).lastOption.getOrElse((Nil, Nil)) + + val baseParams: List[ParamSymbol] = + def defaultBaseParams = baseParams0.map(JustSymbol(_)) + @tailrec + def getRefinedParams(refinedType: Type, level: Int): List[ParamSymbol] = + if level > 0 then + val resultTypeOpt = + refinedType match + case RefinedType(AppliedType(_, args), _, _) => args.lastOption + case AppliedType(_, args) => args.lastOption + case _ => None + resultTypeOpt match + case Some(resultType) => getRefinedParams(resultType, level - 1) + case _ => defaultBaseParams + else + refinedType match + case RefinedType(AppliedType(_, args), _, MethodType(ri)) => + baseParams0.zip(ri).zip(args).map { case ((sym, name), arg) => + RefinedSymbol(sym, name, arg) + } + case _ => defaultBaseParams + // finds param refinements for lambda expressions + // val hello: (x: Int, y: Int) => Unit = (x, _) => println(x) + @tailrec + def refineParams(method: Tree, level: Int): List[ParamSymbol] = + method match + case Select(Apply(f, _), _) => refineParams(f, level + 1) + case Select(h, name) => + // for Select(foo, name = apply) we want `foo.symbol` + if name == nme.apply then getRefinedParams(h.symbol.info, level) + else getRefinedParams(method.symbol.info, level) + case Apply(f, _) => + refineParams(f, level + 1) + case _ => getRefinedParams(method.symbol.info, level) + refineParams(method, 0) + end baseParams + (baseArgs, baseParams) + } + + extension (method: Symbol) + def vparamss(using Context) = method.filteredParamss(_.isTerm) + def tparams(using Context) = method.filteredParamss(_.isType).flatten + def filteredParamss(f: Symbol => Boolean)(using Context) = + method.paramSymss.filter(params => params.forall(f)) +sealed trait ParamSymbol: + def name: Name + def info: Type + def symbol: Symbol + def nameBackticked(using Context) = name.decoded.backticked + +case class JustSymbol(symbol: Symbol)(using Context) extends ParamSymbol: + def name: Name = symbol.name + def info: Type = symbol.info + +case class RefinedSymbol(symbol: Symbol, name: Name, info: Type) + extends ParamSymbol + + +class FuzzyArgMatcher(tparams: List[Symbol])(using Context): + + /** + * A heuristic for checking if the passed arguments match the method's arguments' types. + * For non-polymorphic methods we use the subtype relation (`<:<`) + * and for polymorphic methods we use a heuristic. + * We check the args types not the result type. + */ + def doMatch( + allArgsProvided: Boolean, + span: Span + )(expectedArgs: List[Symbol], actualArgs: List[Tree]) = + (expectedArgs.length == actualArgs.length || + (!allArgsProvided && expectedArgs.length >= actualArgs.length)) && + actualArgs.zipWithIndex.forall { + case (arg: Ident, _) if arg.span.contains(span) => true + case (NamedArg(name, arg), _) => + expectedArgs.exists { expected => + expected.name == name && (!arg.hasType || arg.typeOpt.unfold + .fuzzyArg_<:<(expected.info)) + } + case (arg, i) => + !arg.hasType || arg.typeOpt.unfold.fuzzyArg_<:<(expectedArgs(i).info) + } + + extension (arg: Type) + def fuzzyArg_<:<(expected: Type) = + if tparams.isEmpty then arg <:< expected + else arg <:< substituteTypeParams(expected) + def unfold = + arg match + case arg: TermRef => arg.underlying + case e => e + + private def substituteTypeParams(t: Type): Type = + t match + case e if tparams.exists(_ == e.typeSymbol) => + val matchingParam = tparams.find(_ == e.typeSymbol).get + matchingParam.info match + case b @ TypeBounds(_, _) => WildcardType(b) + case _ => WildcardType + case o @ OrType(e1, e2) => + OrType(substituteTypeParams(e1), substituteTypeParams(e2), o.isSoft) + case AndType(e1, e2) => + AndType(substituteTypeParams(e1), substituteTypeParams(e2)) + case AppliedType(et, eparams) => + AppliedType(et, eparams.map(substituteTypeParams)) + case _ => t + +end FuzzyArgMatcher diff --git a/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala b/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala index 1b44dce8c642..7b30c745e3ed 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/AutoImports.scala @@ -40,7 +40,7 @@ object AutoImports: case class Select(qual: SymbolIdent, name: String) extends SymbolIdent: def value: String = s"${qual.value}.$name" - def direct(name: String): SymbolIdent = Direct(name) + def direct(name: String)(using Context): SymbolIdent = Direct(name) def fullIdent(symbol: Symbol)(using Context): SymbolIdent = val symbols = symbol.ownersIterator.toList @@ -70,7 +70,7 @@ object AutoImports: importSel: Option[ImportSel] ): - def name: String = ident.value + def name(using Context): String = ident.value object SymbolImport: @@ -189,10 +189,13 @@ object AutoImports: ownerImport.importSel, ) else - ( - SymbolIdent.direct(symbol.nameBackticked), - Some(ImportSel.Direct(symbol)), - ) + renames(symbol) match + case Some(rename) => (SymbolIdent.direct(rename), None) + case None => + ( + SymbolIdent.direct(symbol.nameBackticked), + Some(ImportSel.Direct(symbol)), + ) end val SymbolImport( @@ -223,9 +226,13 @@ object AutoImports: importSel ) case None => + val reverse = symbol.ownersIterator.toList.reverse + val fullName = reverse.drop(1).foldLeft(SymbolIdent.direct(reverse.head.nameBackticked)){ + case (acc, sym) => SymbolIdent.Select(acc, sym.nameBackticked(false)) + } SymbolImport( symbol, - SymbolIdent.direct(symbol.fullNameBackticked), + SymbolIdent.Direct(symbol.fullNameBackticked), None ) end match @@ -252,7 +259,6 @@ object AutoImports: val topPadding = if importPosition.padTop then "\n" else "" - val formatted = imports .map { case ImportSel.Direct(sym) => importName(sym) @@ -267,15 +273,16 @@ object AutoImports: end renderImports private def importName(sym: Symbol): String = - if indexedContext.importContext.toplevelClashes(sym) then + if indexedContext.toplevelClashes(sym, inImportScope = true) then s"_root_.${sym.fullNameBackticked(false)}" else sym.ownersIterator.zipWithIndex.foldLeft((List.empty[String], false)) { case ((acc, isDone), (sym, idx)) => if(isDone || sym.isEmptyPackage || sym.isRoot) (acc, true) else indexedContext.rename(sym) match - case Some(renamed) => (renamed :: acc, true) - case None if !sym.isPackageObject => (sym.nameBackticked(false) :: acc, false) - case None => (acc, false) + // we can't import first part + case Some(renamed) if idx != 0 => (renamed :: acc, true) + case _ if !sym.isPackageObject => (sym.nameBackticked(false) :: acc, false) + case _ => (acc, false) }._1.mkString(".") end AutoImportsGenerator diff --git a/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala index e35556ad11c9..97ec396abcf1 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala @@ -4,12 +4,13 @@ import java.nio.file.Paths import scala.collection.mutable import scala.jdk.CollectionConverters.* -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.pc.AutoImportsResultImpl import scala.meta.pc.* import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver import dotty.tools.dotc.util.SourceFile @@ -17,6 +18,7 @@ import dotty.tools.pc.completions.CompletionPos import dotty.tools.pc.utils.InteractiveEnrichments.* import org.eclipse.lsp4j as l +import dotty.tools.dotc.core.Flags.Method final class AutoImportsProvider( search: SymbolSearch, @@ -42,11 +44,22 @@ final class AutoImportsProvider( val path = Interactive.pathTo(newctx.compilationUnit.tpdTree, pos.span)(using newctx) - val indexedContext = IndexedContext( - Interactive.contextOfPath(path)(using newctx) + val indexedContext = IndexedContext(pos)( + using Interactive.contextOfPath(path)(using newctx) ) import indexedContext.ctx + + def correctInTreeContext(sym: Symbol) = path match + case (_: Ident) :: (sel: Select) :: _ => + sym.info.allMembers.exists(_.name == sel.name) + case (_: Ident) :: (_: Apply) :: _ if !sym.is(Method) => + def applyInObject = + sym.companionModule.info.allMembers.exists(_.name == nme.apply) + def applyInClass = sym.info.allMembers.exists(_.name == nme.apply) + applyInClass || applyInObject + case _ => true + val isSeen = mutable.Set.empty[String] val symbols = List.newBuilder[Symbol] def visit(sym: Symbol): Boolean = @@ -83,20 +96,31 @@ final class AutoImportsProvider( text, tree, unit.comments, - indexedContext.importContext, + indexedContext, config ) (sym: Symbol) => generator.forSymbol(sym) end match end mkEdit - for + val all = for sym <- results edits <- mkEdit(sym) - yield AutoImportsResultImpl( + yield (AutoImportsResultImpl( sym.owner.showFullName, edits.asJava - ) + ), sym) + + all match + case (onlyResult, _) :: Nil => List(onlyResult) + case Nil => Nil + case moreResults => + val moreExact = moreResults.filter { case (_, sym) => + correctInTreeContext(sym) + } + if moreExact.nonEmpty then moreExact.map(_._1) + else moreResults.map(_._1) + else List.empty end if end autoImports diff --git a/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala b/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala index 9fb84ee1f513..54e0ca3fc4a9 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala @@ -4,7 +4,7 @@ import java.util.logging.Level import java.util.logging.Logger import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.pc.* import scala.util.control.NonFatal @@ -34,8 +34,8 @@ class CompilerSearchVisitor( logger.log(Level.WARNING, err.getMessage()) false case NonFatal(e) => - reports.incognito.create( - Report( + reports.incognito.nn.create( + () => Report( "is_public", s"""Symbol: $sym""".stripMargin, e diff --git a/presentation-compiler/src/main/dotty/tools/pc/CompletionItemResolver.scala b/presentation-compiler/src/main/dotty/tools/pc/CompletionItemResolver.scala index 291ffe1fec30..f7fdb1c36e6d 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/CompletionItemResolver.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/CompletionItemResolver.scala @@ -62,7 +62,7 @@ object CompletionItemResolver extends ItemResolver: if companion == NoSymbol || gsym.is(JavaDefined) then if gsymDoc.isEmpty() then if gsym.isAliasType then - fullDocstring(gsym.info.deepDealias.typeSymbol, search) + fullDocstring(gsym.info.deepDealiasAndSimplify.typeSymbol, search) else if gsym.is(Method) then gsym.info.finalResultType match case tr @ TermRef(_, sym) => @@ -75,7 +75,7 @@ object CompletionItemResolver extends ItemResolver: else gsymDoc else val companionDoc = docs(companion) - if companionDoc.isEmpty() then gsymDoc + if companionDoc.isEmpty() || companionDoc == gsymDoc then gsymDoc else if gsymDoc.isEmpty() then companionDoc else List( diff --git a/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala index c72a0602f1ce..bd44878aa11a 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ExtractMethodProvider.scala @@ -2,7 +2,7 @@ package dotty.tools.pc import java.nio.file.Paths -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.pc.ExtractMethodUtils import scala.meta.pc.OffsetParams import scala.meta.pc.RangeParams @@ -51,7 +51,7 @@ final class ExtractMethodProvider( given locatedCtx: Context = val newctx = driver.currentCtx.fresh.setCompilationUnit(unit) Interactive.contextOfPath(path)(using newctx) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val printer = ShortenedTypePrinter(search, IncludeDefaultParam.Never)(using indexedCtx) def prettyPrint(tpe: Type) = diff --git a/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala index a667b038c894..4ac07587fce2 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/HoverProvider.scala @@ -3,7 +3,7 @@ package dotty.tools.pc import java.util as ju import scala.meta.internal.metals.Report -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.pc.ScalaHover import scala.meta.pc.ContentType import scala.meta.pc.HoverSignature @@ -13,6 +13,7 @@ import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Constants.* import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Decorators.* import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.StdNames.* @@ -47,7 +48,7 @@ object HoverProvider: val path = unit .map(unit => Interactive.pathTo(unit.tpdTree, pos.span)) .getOrElse(Interactive.pathTo(driver.openedTrees(uri), pos)) - val indexedContext = IndexedContext(ctx) + val indexedContext = IndexedContext(pos)(using ctx) def typeFromPath(path: List[Tree]) = if path.isEmpty then NoType else path.head.typeOpt @@ -86,7 +87,7 @@ object HoverProvider: s"$uri::$posId" ) end report - reportContext.unsanitized.create(report, ifVerbose = true) + reportContext.unsanitized.nn.create(() => report, /*ifVerbose =*/ true) ju.Optional.empty().nn else val skipCheckOnName = @@ -94,7 +95,7 @@ object HoverProvider: val printerCtx = Interactive.contextOfPath(path) val printer = ShortenedTypePrinter(search, IncludeDefaultParam.Include)( - using IndexedContext(printerCtx) + using IndexedContext(pos)(using printerCtx) ) MetalsInteractive.enclosingSymbolsWithExpressionType( enclosing, @@ -104,11 +105,11 @@ object HoverProvider: ) match case Nil => fallbackToDynamics(path, printer, contentType) - case (symbol, tpe) :: _ + case (symbol, tpe, _) :: _ if symbol.name == nme.selectDynamic || symbol.name == nme.applyDynamic => fallbackToDynamics(path, printer, contentType) - case symbolTpes @ ((symbol, tpe) :: _) => - val exprTpw = tpe.widenTermRefExpr.deepDealias + case symbolTpes @ ((symbol, tpe, _) :: _) => + val exprTpw = tpe.widenTermRefExpr.deepDealiasAndSimplify val hoverString = tpw match // https://github.com/lampepfl/dotty/issues/8891 @@ -123,7 +124,7 @@ object HoverProvider: if tpe != NoType then tpe else tpw - printer.hoverSymbol(sym, finalTpe.deepDealias) + printer.hoverSymbol(sym, finalTpe.deepDealiasAndSimplify) end match end hoverString @@ -131,7 +132,12 @@ object HoverProvider: .flatMap(symTpe => search.symbolDocumentation(symTpe._1, contentType)) .map(_.docstring()) .mkString("\n") - printer.expressionType(exprTpw) match + + val expresionTypeOpt = + if symbol.name == nme.??? then + InferExpectedType(search, driver, params).infer() + else printer.expressionType(exprTpw) + expresionTypeOpt match case Some(expressionType) => val forceExpressionType = !pos.span.isZeroExtent || ( @@ -165,23 +171,30 @@ object HoverProvider: printer: ShortenedTypePrinter, contentType: ContentType )(using Context): ju.Optional[HoverSignature] = path match - case SelectDynamicExtractor(sel, n, name) => + case SelectDynamicExtractor(sel, n, name, rest) => def findRefinement(tp: Type): Option[HoverSignature] = tp match case RefinedType(_, refName, tpe) if name == refName.toString() => + val resultType = + rest match + case Select(_, asInstanceOf) :: TypeApply(_, List(tpe)) :: _ if asInstanceOf == nme.asInstanceOfPM => + tpe.tpe.widenTermRefExpr.deepDealiasAndSimplify + case _ if n == nme.selectDynamic => tpe.resultType + case _ => tpe + val tpeString = - if n == nme.selectDynamic then s": ${printer.tpe(tpe.resultType)}" - else printer.tpe(tpe) + if n == nme.selectDynamic then s": ${printer.tpe(resultType)}" + else printer.tpe(resultType) val valOrDef = if n == nme.selectDynamic && !tpe.isInstanceOf[ExprType] - then "val" - else "def" + then "val " + else "def " Some( new ScalaHover( expressionType = Some(tpeString), - symbolSignature = Some(s"$valOrDef $name$tpeString"), + symbolSignature = Some(s"$valOrDef$name$tpeString"), contextInfo = printer.getUsedRenamesInfo, contentType = contentType ) @@ -190,12 +203,21 @@ object HoverProvider: findRefinement(parent) case _ => None - val refTpe = sel.typeOpt.widen.deepDealias match - case r: RefinedType => Some(r) - case t: (TermRef | TypeProxy) => Some(t.termSymbol.info.deepDealias) - case _ => None - - refTpe.flatMap(findRefinement).asJava + def extractRefinements(t: Type): List[Type] = t match + case r: RefinedType => List(r) + case t: (TypeRef | AppliedType) => + // deepDealiasAndSimplify can succeed with no progress, so we have to avoid infinite loops + val t1 = t.deepDealiasAndSimplify + if t1 == t then Nil + else extractRefinements(t1) + case t: TermRef => extractRefinements(t.widen) + case t: TypeProxy => List(t.termSymbol.info.deepDealiasAndSimplify) + case AndType(l , r) => List(extractRefinements(l), extractRefinements(r)).flatten + case _ => Nil + + val refTpe: List[Type] = extractRefinements(sel.typeOpt) + + refTpe.flatMap(findRefinement).headOption.asJava case _ => ju.Optional.empty().nn @@ -208,16 +230,16 @@ object SelectDynamicExtractor: case Select(_, _) :: Apply( Select(Apply(reflSel, List(sel)), n), List(Literal(Constant(name: String))) - ) :: _ + ) :: rest if (n == nme.selectDynamic || n == nme.applyDynamic) && nme.reflectiveSelectable == reflSel.symbol.name => - Some(sel, n, name) + Some(sel, n, name, rest) // tests `selectable`, `selectable2` and `selectable-full` in HoverScala3TypeSuite case Select(_, _) :: Apply( Select(sel, n), List(Literal(Constant(name: String))) - ) :: _ if n == nme.selectDynamic || n == nme.applyDynamic => - Some(sel, n, name) + ) :: rest if n == nme.selectDynamic || n == nme.applyDynamic => + Some(sel, n, name, rest) case _ => None end match end unapply diff --git a/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala b/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala index 7c2c34cf5ebb..3fbfafff3e38 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala @@ -4,64 +4,45 @@ import scala.annotation.tailrec import scala.util.control.NonFatal import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Denotations.PreDenotation +import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.core.Flags.* -import dotty.tools.dotc.core.NameOps.moduleClassName +import dotty.tools.dotc.core.NameOps.* import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.Scopes.EmptyScope import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.interactive.Completion import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.typer.ImportInfo +import dotty.tools.dotc.util.SourcePosition import dotty.tools.pc.IndexedContext.Result import dotty.tools.pc.utils.InteractiveEnrichments.* sealed trait IndexedContext: given ctx: Context def scopeSymbols: List[Symbol] - def names: IndexedContext.Names def rename(sym: Symbol): Option[String] - def outer: IndexedContext - - def findSymbol(name: String): Option[List[Symbol]] - - final def findSymbol(name: Name): Option[List[Symbol]] = - findSymbol(name.decoded) - - final def lookupSym(sym: Symbol): Result = - findSymbol(sym.decodedName) match - case Some(symbols) if symbols.exists(_ == sym) => - Result.InScope - case Some(symbols) - if symbols.exists(s => isNotConflictingWithDefault(s, sym) || isTypeAliasOf(s, sym) || isTermAliasOf(s, sym)) => - Result.InScope - // when all the conflicting symbols came from an old version of the file + def findSymbol(name: Name, fromPrefix: Option[Type] = None): Option[List[Symbol]] + def findSymbolInLocalScope(name: String): Option[List[Symbol]] + + final def lookupSym(sym: Symbol, fromPrefix: Option[Type] = None): Result = + def all(symbol: Symbol): Set[Symbol] = Set(symbol, symbol.companionModule, symbol.companionClass, symbol.companion).filter(_ != NoSymbol) + val isRelated = all(sym) ++ all(sym.dealiasType) + findSymbol(sym.name, fromPrefix) match + case Some(symbols) if symbols.exists(isRelated) => Result.InScope + case Some(symbols) if symbols.exists(isTermAliasOf(_, sym)) => Result.InScope + case Some(symbols) if symbols.map(_.dealiasType).exists(isRelated) => Result.InScope case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) => Result.Missing case Some(symbols) if symbols.exists(rename(_).isEmpty) => Result.Conflict + case Some(symbols) => Result.InScope case _ => Result.Missing end lookupSym - /** - * Scala by default imports following packages: - * https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html - * import java.lang.* - * { - * import scala.* - * { - * import Predef.* - * { /* source */ } - * } - * } - * - * This check is necessary for proper scope resolution, because when we compare symbols from - * index including the underlying type like scala.collection.immutable.List it actually - * is in current scope in form of type forwarder imported from Predef. - */ - private def isNotConflictingWithDefault(sym: Symbol, queriedSym: Symbol): Boolean = - sym.info.widenDealias =:= queriedSym.info.widenDealias && (Interactive.isImportedByDefault(sym)) - final def hasRename(sym: Symbol, as: String): Boolean = rename(sym) match - case Some(v) => v == as + case Some(v) => + v == as case None => false // detects import scope aliases like @@ -74,73 +55,94 @@ sealed trait IndexedContext: case _ => false ) - private def isTypeAliasOf(alias: Symbol, queriedSym: Symbol): Boolean = - alias.isAliasType && alias.info.deepDealias.typeSymbol == queriedSym - - final def isEmpty: Boolean = this match - case IndexedContext.Empty => true - case _ => false - - final def importContext: IndexedContext = - this match - case IndexedContext.Empty => this - case _ if ctx.owner.is(Package) => this - case _ => outer.importContext - @tailrec - final def toplevelClashes(sym: Symbol): Boolean = + final def toplevelClashes(sym: Symbol, inImportScope: Boolean): Boolean = if sym == NoSymbol || sym.owner == NoSymbol || sym.owner.isRoot then - lookupSym(sym) match - case IndexedContext.Result.Conflict => true + val possibleConflictingSymbols = findSymbolInLocalScope(sym.name.show) + // if it's import scope we only care about toplevel conflicts, not any clashes inside objects etc. + val symbolClashes = if inImportScope then + // It's toplevel if it's parent is a package + possibleConflictingSymbols.filter(_.exists(_.owner.is(Package))) + else + possibleConflictingSymbols + symbolClashes match + case Some(symbols) if !symbols.contains(sym) => true case _ => false - else toplevelClashes(sym.owner) + else toplevelClashes(sym.owner, inImportScope) end IndexedContext object IndexedContext: - def apply(ctx: Context): IndexedContext = + def apply(pos: SourcePosition)(using Context): IndexedContext = ctx match case NoContext => Empty - case _ => LazyWrapper(using ctx) + case _ => LazyWrapper(pos)(using ctx) case object Empty extends IndexedContext: given ctx: Context = NoContext - def findSymbol(name: String): Option[List[Symbol]] = None + def findSymbol(name: Name, fromPrefix: Option[Type]): Option[List[Symbol]] = None + def findSymbolInLocalScope(name: String): Option[List[Symbol]] = None def scopeSymbols: List[Symbol] = List.empty - val names: Names = Names(Map.empty, Map.empty) def rename(sym: Symbol): Option[String] = None - def outer: IndexedContext = this - - class LazyWrapper(using val ctx: Context) extends IndexedContext: - val outer: IndexedContext = IndexedContext(ctx.outer) - val names: Names = extractNames(ctx) - def findSymbol(name: String): Option[List[Symbol]] = - names.symbols - .get(name) - .map(_.toList) - .orElse(outer.findSymbol(name)) + class LazyWrapper(pos: SourcePosition)(using val ctx: Context) extends IndexedContext: + + val completionContext = Completion.scopeContext(pos) + val names: Map[String, Seq[SingleDenotation]] = completionContext.names.toList.groupBy(_._1.show).map{ + case (name, denotations) => + val denots = denotations.flatMap(_._2) + val nonRoot = denots.filter(!_.symbol.owner.isRoot) + val (importedByDefault, conflictingValue) = denots.partition(denot => Interactive.isImportedByDefault(denot.symbol)) + if importedByDefault.nonEmpty && conflictingValue.nonEmpty then + name.trim.nn -> conflictingValue + else + name.trim.nn -> nonRoot + } + val renames = completionContext.renames + + def defaultScopes(name: Name): Option[List[Symbol]] = + List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass) + .map(_.membersNamed(name)) + .collect { case denot if denot.exists => denot.first.symbol } + .toList match + case Nil => None + case list => Some(list) + + override def findSymbolInLocalScope(name: String): Option[List[Symbol]] = + names.get(name).map(_.map(_.symbol).toList).filter(_.nonEmpty) + def findSymbol(name: Name, fromPrefix: Option[Type]): Option[List[Symbol]] = + names + .get(name.show) + .map { denots => + def skipThisType(tp: Type): Type = tp match + case ThisType(prefix) => skipThisType(prefix) + case _ => tp + + val filteredDenots = fromPrefix match + case Some(prefix) => + val target = skipThisType(prefix) + denots.filter { denot => + denot.prefix == NoPrefix || + (denot.prefix match + case tref: TermRef => + tref.termSymbol.info <:< target + case otherPrefix => + otherPrefix <:< target + ) + } + case None => denots + + filteredDenots.map(_.symbol).toList + } + .orElse(defaultScopes(name)).filter(_.nonEmpty) def scopeSymbols: List[Symbol] = - val acc = Set.newBuilder[Symbol] - (this :: outers).foreach { ref => - acc ++= ref.names.symbols.values.flatten - } - acc.result.toList + names.values.flatten.map(_.symbol).toList def rename(sym: Symbol): Option[String] = - names.renames - .get(sym) - .orElse(outer.rename(sym)) - - private def outers: List[IndexedContext] = - val builder = List.newBuilder[IndexedContext] - var curr = outer - while !curr.isEmpty do - builder += curr - curr = curr.outer - builder.result + renames.get(sym).orElse(renames.get(sym.companion)).map(_.decoded) + end LazyWrapper enum Result: @@ -149,97 +151,5 @@ object IndexedContext: case InScope | Conflict => true case Missing => false - case class Names( - symbols: Map[String, List[Symbol]], - renames: Map[Symbol, String] - ) - - private def extractNames(ctx: Context): Names = - def isAccessibleFromSafe(sym: Symbol, site: Type): Boolean = - try sym.isAccessibleFrom(site, superAccess = false) - catch - case NonFatal(e) => - false - - def accessibleSymbols(site: Type, tpe: Type)(using - Context - ): List[Symbol] = - tpe.decls.toList.filter(sym => isAccessibleFromSafe(sym, site)) - - def accesibleMembers(site: Type)(using Context): List[Symbol] = - site.allMembers - .filter(denot => - try isAccessibleFromSafe(denot.symbol, site) - catch - case NonFatal(e) => - false - ) - .map(_.symbol) - .toList - - def allAccessibleSymbols( - tpe: Type, - filter: Symbol => Boolean = _ => true - )(using Context): List[Symbol] = - val initial = accessibleSymbols(tpe, tpe).filter(filter) - val fromPackageObjects = - initial - .filter(_.isPackageObject) - .flatMap(sym => accessibleSymbols(tpe, sym.thisType)) - initial ++ fromPackageObjects - - def fromImport(site: Type, name: Name)(using Context): List[Symbol] = - List( - site.member(name.toTypeName), - site.member(name.toTermName), - site.member(name.moduleClassName), - ) - .flatMap(_.alternatives) - .map(_.symbol) - - def fromImportInfo( - imp: ImportInfo - )(using Context): List[(Symbol, Option[TermName])] = - val excludedNames = imp.excluded.map(_.decoded) - - if imp.isWildcardImport then - allAccessibleSymbols( - imp.site, - sym => !excludedNames.contains(sym.name.decoded) - ).map((_, None)) - else - imp.forwardMapping.toList.flatMap { (name, rename) => - val isRename = name != rename - if !isRename && !excludedNames.contains(name.decoded) then - fromImport(imp.site, name).map((_, None)) - else if isRename then - fromImport(imp.site, name).map((_, Some(rename))) - else Nil - } - end if - end fromImportInfo - - given Context = ctx - val (symbols, renames) = - if ctx.isImportContext then - val (syms, renames) = - fromImportInfo(ctx.importInfo.nn) - .map((sym, rename) => (sym, rename.map(r => sym -> r.decoded))) - .unzip - (syms, renames.flatten.toMap) - else if ctx.owner.isClass then - val site = ctx.owner.thisType - (accesibleMembers(site), Map.empty) - else if ctx.scope != EmptyScope then (ctx.scope.toList, Map.empty) - else (List.empty, Map.empty) - - val initial = Map.empty[String, List[Symbol]] - val values = - symbols.foldLeft(initial) { (acc, sym) => - val name = sym.decodedName - val syms = acc.getOrElse(name, List.empty) - acc.updated(name, sym :: syms) - } - Names(values, renames) - end extractNames + end IndexedContext diff --git a/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala b/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala index d121c8059286..721b9c6375ab 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/InferExpectedType.scala @@ -1,15 +1,11 @@ package dotty.tools.pc -import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.ast.tpd.* -import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.StdNames -import dotty.tools.dotc.core.Symbols import dotty.tools.dotc.core.Symbols.defn import dotty.tools.dotc.core.Types.* -import dotty.tools.dotc.core.Types.Type import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver import dotty.tools.dotc.typer.Applications.unapplyArgs @@ -21,7 +17,7 @@ import dotty.tools.pc.printer.ShortenedTypePrinter import dotty.tools.pc.printer.ShortenedTypePrinter.IncludeDefaultParam import dotty.tools.pc.utils.InteractiveEnrichments.* -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.pc.OffsetParams import scala.meta.pc.SymbolSearch @@ -51,15 +47,15 @@ class InferExpectedType( ) val locatedCtx = Interactive.contextOfPath(tpdPath)(using newctx) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val printer = ShortenedTypePrinter(search, IncludeDefaultParam.ResolveLater)(using indexedCtx) - InterCompletionType.inferType(path)(using newctx).map{ + InferCompletionType.inferType(path)(using newctx).map{ tpe => printer.tpe(tpe) } case None => None -object InterCompletionType: +object InferCompletionType: def inferType(path: List[Tree])(using Context): Option[Type] = path match case (lit: Literal) :: Select(Literal(_), _) :: Apply(Select(Literal(_), _), List(s: Select)) :: rest if s.symbol == defn.Predef_undefined => inferType(rest, lit.span) @@ -76,7 +72,7 @@ object InterCompletionType: case Try(block, _, _) :: rest if block.span.contains(span) => inferType(rest, span) case CaseDef(_, _, body) :: Try(_, cases, _) :: rest if body.span.contains(span) && cases.exists(_.span.contains(span)) => inferType(rest, span) case If(cond, _, _) :: rest if !cond.span.contains(span) => inferType(rest, span) - case If(cond, _, _) :: rest if cond.span.contains(span) => Some(Symbols.defn.BooleanType) + case If(cond, _, _) :: rest if cond.span.contains(span) => Some(defn.BooleanType) case CaseDef(_, _, body) :: Match(_, cases) :: rest if body.span.contains(span) && cases.exists(_.span.contains(span)) => inferType(rest, span) case NamedArg(_, arg) :: rest if arg.span.contains(span) => inferType(rest, span) @@ -97,39 +93,8 @@ object InterCompletionType: if ind < 0 then None else unapplyArgs(fun.tpe.finalResultType, fun, pats, NoSourcePosition).lift(ind) // f(@@) - case (app: Apply) :: rest => - val param = - for { - ind <- app.args.zipWithIndex.collectFirst { - case (arg, id) if arg.span.contains(span) => id - } - params <- app.symbol.paramSymss.find(!_.exists(_.isTypeParam)) - param <- params.get(ind) - } yield param.info - param match - // def f[T](a: T): T = ??? - // f[Int](@@) - // val _: Int = f(@@) - case Some(t : TypeRef) if t.symbol.is(Flags.TypeParam) => - for { - (typeParams, args) <- - app match - case Apply(TypeApply(fun, args), _) => - val typeParams = fun.symbol.paramSymss.headOption.filter(_.forall(_.isTypeParam)) - typeParams.map((_, args.map(_.tpe))) - // val f: (j: "a") => Int - // f(@@) - case Apply(Select(v, StdNames.nme.apply), _) => - v.symbol.info match - case AppliedType(des, args) => - Some((des.typeSymbol.typeParams, args)) - case _ => None - case _ => None - ind = typeParams.indexOf(t.symbol) - tpe <- args.get(ind) - if !tpe.isErroneous - } yield tpe - case Some(tpe) => Some(tpe) - case _ => None + case ApplyExtractor(app) => + val idx = app.args.indexWhere(_.span.contains(span)) + app.fun.tpe.widenTermRefExpr.paramInfoss.flatten.get(idx) case _ => None diff --git a/presentation-compiler/src/main/dotty/tools/pc/InferredMethodProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/InferredMethodProvider.scala new file mode 100644 index 000000000000..d4299f6b1f95 --- /dev/null +++ b/presentation-compiler/src/main/dotty/tools/pc/InferredMethodProvider.scala @@ -0,0 +1,362 @@ +package dotty.tools.pc + +import java.nio.file.Paths + +import scala.annotation.tailrec + +import scala.meta.pc.OffsetParams +import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.SymbolSearch +import scala.meta.pc.reports.ReportContext + +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.Symbols.defn +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.interactive.Interactive +import dotty.tools.dotc.interactive.InteractiveDriver +import dotty.tools.dotc.util.SourceFile +import dotty.tools.dotc.util.SourcePosition +import dotty.tools.pc.printer.ShortenedTypePrinter +import dotty.tools.pc.printer.ShortenedTypePrinter.IncludeDefaultParam +import dotty.tools.pc.utils.InteractiveEnrichments.* + +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j as l + +/** + * Tries to calculate edits needed to create a method that will fix missing symbol + * in all the places that it is possible such as: + * - apply inside method invocation `method(.., nonExistent(param), ...)` and `method(.., nonExistent, ...)` + * - method in val definition `val value: DefinedType = nonExistent(param)` and `val value: DefinedType = nonExistent` + * - simple method call `nonExistent(param)` and `nonExistent` + * - method call inside a container `container.nonExistent(param)` and `container.nonExistent` + * + * @param params position and actual source + * @param driver Scala 3 interactive compiler driver + * @param config presentation compiler configuration + * @param symbolSearch symbol search + */ +final class InferredMethodProvider( + params: OffsetParams, + driver: InteractiveDriver, + config: PresentationCompilerConfig, + symbolSearch: SymbolSearch +)(using ReportContext): + + case class AdjustTypeOpts( + text: String, + adjustedEndPos: l.Position + ) + + def inferredMethodEdits( + adjustOpt: Option[AdjustTypeOpts] = None + ): List[TextEdit] = + val uri = params.uri().nn + val filePath = Paths.get(uri).nn + + val sourceText = adjustOpt.map(_.text).getOrElse(params.text().nn) + val source = + SourceFile.virtual(filePath.toString(), sourceText) + driver.run(uri, source) + val unit = driver.currentCtx.run.nn.units.head + val pos = driver.sourcePosition(params) + val path = + Interactive.pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx) + + given locatedCtx: Context = driver.localContext(params) + val indexedCtx = IndexedContext(pos)(using locatedCtx) + + val autoImportsGen = AutoImports.generator( + pos, + sourceText, + unit.tpdTree, + unit.comments, + indexedCtx, + config + ) + + val printer = ShortenedTypePrinter( + symbolSearch, + includeDefaultParam = IncludeDefaultParam.ResolveLater, + isTextEdit = true + )(using indexedCtx) + + def imports: List[TextEdit] = + printer.imports(autoImportsGen) + + def printType(tpe: Type): String = + printer.tpe(tpe) + + def printName(name: Name): String = + printer.nameString(name) + + def printParams(params: List[Type], startIndex: Int = 0): String = + params.zipWithIndex + .map { case (p, index) => + s"arg${index + startIndex}: ${printType(p)}" + } + .mkString(", ") + + def printSignature( + methodName: Name, + params: List[List[Type]], + retTypeOpt: Option[Type] + ): String = + val retTypeString = retTypeOpt match + case Some(retType) => + val printRetType = printType(retType) + if retType.isAny then "" + else s": $printRetType" + case _ => "" + + val (paramsString, _) = params.foldLeft(("", 0)){ + case ((acc, startIdx), paramList) => + val printed = s"(${printParams(paramList, startIdx)})" + (acc + printed, startIdx + paramList.size) + } + + s"def ${printName(methodName)}$paramsString$retTypeString = ???" + + @tailrec + def countIndent(text: String, index: Int, acc: Int): Int = + if index > 0 && text(index) != '\n' then countIndent(text, index - 1, acc + 1) + else acc + + def indentation(text: String, pos: Int): String = + if pos > 0 then + val isSpace = text(pos) == ' ' + val isTab = text(pos) == '\t' + val indent = countIndent(params.text().nn, pos, 0) + + if isSpace then " " * indent else if isTab then "\t" * indent else "" + else "" + + def insertPosition() = + val blockOrTemplateIndex = + path.tail.indexWhere { + case _: Block | _: Template => true + case _ => false + } + path(blockOrTemplateIndex).sourcePos + + /** + * Returns the position to insert the method signature for a container. + * If the container has an empty body, the position is the end of the container. + * If the container has a non-empty body, the position is the end of the last element in the body. + * + * @param container the container to insert the method signature for + * @return the position to insert the method signature for the container and a boolean indicating if the container has an empty body + */ + def insertPositionFor(container: Tree): Option[(SourcePosition, Boolean)] = + val typeSymbol = container.tpe.widenDealias.typeSymbol + if typeSymbol.exists then + val trees = driver.openedTrees(params.uri().nn) + val include = Interactive.Include.definitions | Interactive.Include.local + Interactive.findTreesMatching(trees, include, typeSymbol).headOption match + case Some(srcTree) => + srcTree.tree match + case classDef: TypeDef if classDef.rhs.isInstanceOf[Template] => + val template = classDef.rhs.asInstanceOf[Template] + val (pos, hasEmptyBody) = template.body.lastOption match + case Some(last) => (last.sourcePos, false) + case None => (classDef.sourcePos, true) + Some((pos, hasEmptyBody)) + case _ => None + case None => None + else None + + /** + * Extracts type information for a specific parameter in a method signature. + * If the parameter is a function type, extracts both the function's argument types + * and return type. Otherwise, extracts just the parameter type. + * + * @param methodType the method type to analyze + * @param argIndex the index of the parameter to extract information for + * @return a tuple of (argument types, return type) where: + * - argument types: Some(List[Type]) if parameter is a function, None otherwise + * - return type: Some(Type) representing either the function's return type or the parameter type itself + */ + def extractParameterTypeInfo(methodType: Type, argIndex: Int): (Option[List[Type]], Option[Type]) = + methodType match + case m @ MethodType(param) => + val expectedFunctionType = m.paramInfos(argIndex) + if defn.isFunctionType(expectedFunctionType) then + expectedFunctionType match + case defn.FunctionOf(argTypes, retType, _) => + (Some(argTypes), Some(retType)) + case _ => + (None, Some(expectedFunctionType)) + else + (None, Some(m.paramInfos(argIndex))) + case _ => (None, None) + + def signatureEdits(signature: String): List[TextEdit] = + val pos = insertPosition() + val indent = indentation(params.text().nn, pos.start - 1) + val lspPos = pos.toLsp + lspPos.setEnd(lspPos.getStart()) + + List( + TextEdit( + lspPos, + s"$signature\n$indent", + ) + ) ::: imports + + def signatureEditsForContainer(signature: String, container: Tree): List[TextEdit] = + insertPositionFor(container) match + case Some((pos, hasEmptyBody)) => + val lspPos = pos.toLsp + lspPos.setStart(lspPos.getEnd()) + val indent = indentation(params.text().nn, pos.start - 1) + + if hasEmptyBody then + List( + TextEdit( + lspPos, + s":\n $indent$signature", + ) + ) ::: imports + else + List( + TextEdit( + lspPos, + s"\n$indent$signature", + ) + ) ::: imports + case None => Nil + + path match + /** + * outerArgs + * --------------------------- + * method(..., errorMethod(args), ...) + * + */ + case (id @ Ident(errorMethod)) :: + (apply @ Apply(func, args)) :: + Apply(method, outerArgs) :: + _ if id.symbol == NoSymbol && func == id && method != apply => + + val argTypes = args.map(_.typeOpt.widenDealias) + + val argIndex = outerArgs.indexOf(apply) + val (allArgTypes, retTypeOpt) = + extractParameterTypeInfo(method.tpe.widenDealias, argIndex) match + case (Some(argTypes2), retTypeOpt) => (List(argTypes, argTypes2), retTypeOpt) + case (None, retTypeOpt) => (List(argTypes), retTypeOpt) + + val signature = printSignature(errorMethod, allArgTypes, retTypeOpt) + + signatureEdits(signature) + + /** + * outerArgs + * --------------------- + * method(..., errorMethod, ...) + * + */ + case (id @ Ident(errorMethod)) :: + Apply(method, outerArgs) :: + _ if id.symbol == NoSymbol && method != id => + + val argIndex = outerArgs.indexOf(id) + + val (argTypes, retTypeOpt) = extractParameterTypeInfo(method.tpe.widenDealias, argIndex) + + val allArgTypes = argTypes match + case Some(argTypes) => List(argTypes) + case None => Nil + + val signature = printSignature(errorMethod, allArgTypes, retTypeOpt) + + signatureEdits(signature) + + /** + * tpt body + * ----------- ---------------- + * val value: DefinedType = errorMethod(args) + * + */ + case (id @ Ident(errorMethod)) :: + (apply @ Apply(func, args)) :: + ValDef(_, tpt, body) :: + _ if id.symbol == NoSymbol && func == id && apply == body => + + val retType = tpt.tpe.widenDealias + val argTypes = args.map(_.typeOpt.widenDealias) + + val signature = printSignature(errorMethod, List(argTypes), Some(retType)) + signatureEdits(signature) + + /** + * tpt body + * ----------- ----------- + * val value: DefinedType = errorMethod + * + */ + case (id @ Ident(errorMethod)) :: + ValDef(_, tpt, body) :: + _ if id.symbol == NoSymbol && id == body => + + val retType = tpt.tpe.widenDealias + + val signature = printSignature(errorMethod, Nil, Some(retType)) + signatureEdits(signature) + + /** + * + * errorMethod(args) + * + */ + case (id @ Ident(errorMethod)) :: + (apply @ Apply(func, args)) :: + _ if id.symbol == NoSymbol && func == id => + + val argTypes = args.map(_.typeOpt.widenDealias) + + val signature = printSignature(errorMethod, List(argTypes), None) + signatureEdits(signature) + + /** + * + * errorMethod + * + */ + case (id @ Ident(errorMethod)) :: + _ if id.symbol == NoSymbol => + + val signature = printSignature(errorMethod, Nil, None) + signatureEdits(signature) + + /** + * + * container.errorMethod(args) + * + */ + case (select @ Select(container, errorMethod)) :: + (apply @ Apply(func, args)) :: + _ if select.symbol == NoSymbol && func == select => + + val argTypes = args.map(_.typeOpt.widenDealias) + val signature = printSignature(errorMethod, List(argTypes), None) + signatureEditsForContainer(signature, container) + + /** + * + * container.errorMethod + * + */ + case (select @ Select(container, errorMethod)) :: + _ if select.symbol == NoSymbol => + + val signature = printSignature(errorMethod, Nil, None) + signatureEditsForContainer(signature, container) + + case _ => Nil + + end inferredMethodEdits +end InferredMethodProvider diff --git a/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala index d8cdbcd8fe69..0ab1d264828e 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/InferredTypeProvider.scala @@ -3,7 +3,7 @@ package dotty.tools.pc import java.nio.file.Paths import scala.annotation.tailrec -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.SymbolSearch @@ -75,7 +75,7 @@ final class InferredTypeProvider( Interactive.pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx) given locatedCtx: Context = driver.localContext(params) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val autoImportsGen = AutoImports.generator( pos, sourceText, @@ -94,14 +94,15 @@ final class InferredTypeProvider( tpe match case tref: TypeRef => indexedCtx.lookupSym( - tref.currentSymbol + tref.currentSymbol, + Some(tref.prefix) ) == IndexedContext.Result.InScope case AppliedType(tycon, args) => isInScope(tycon) && args.forall(isInScope) case _ => true if isInScope(tpe) then tpe - else tpe.deepDealias + else tpe.deepDealiasAndSimplify val printer = ShortenedTypePrinter( symbolSearch, @@ -112,8 +113,8 @@ final class InferredTypeProvider( def imports: List[TextEdit] = printer.imports(autoImportsGen) - def printType(tpe: Type): String = - printer.tpe(tpe) + def printTypeAscription(tpe: Type, spaceBefore: Boolean = false): String = + (if spaceBefore then " : " else ": ") + printer.tpe(tpe) path.headOption match /* `val a = 1` or `var b = 2` @@ -124,7 +125,7 @@ final class InferredTypeProvider( * turns into * `.map((a: Int) => a + a)` */ - case Some(vl @ ValDef(sym, tpt, rhs)) => + case Some(vl @ ValDef(name, tpt, rhs)) => val isParam = path match case head :: next :: _ if next.symbol.isAnonymousFunction => true case head :: (b @ Block(stats, expr)) :: next :: _ @@ -136,9 +137,10 @@ final class InferredTypeProvider( val endPos = findNamePos(sourceText, vl, keywordOffset).endPos.toLsp adjustOpt.foreach(adjust => endPos.setEnd(adjust.adjustedEndPos)) + val spaceBefore = name.isOperatorName new TextEdit( endPos, - ": " + printType(optDealias(tpt.typeOpt)) + { + printTypeAscription(optDealias(tpt.typeOpt), spaceBefore) + { if withParens then ")" else "" } ) @@ -181,8 +183,7 @@ final class InferredTypeProvider( typeNameEdit ::: imports rhs match - case t: Tree[?] - if t.typeOpt.isErroneous && retryType && !tpt.sourcePos.span.isZeroExtent => + case t: Tree[?] if !tpt.sourcePos.span.isZeroExtent => inferredTypeEdits( Some( AdjustTypeOpts( @@ -197,7 +198,7 @@ final class InferredTypeProvider( * turns into * `def a[T](param : Int): Int = param` */ - case Some(df @ DefDef(name, _, tpt, rhs)) => + case Some(df @ DefDef(name, paramss, tpt, rhs)) => def typeNameEdit = /* NOTE: In Scala 3.1.3, `List((1,2)).map((<>,b) => ...)` * turns into `List((1,2)).map((:Inta,b) => ...)`, @@ -208,10 +209,12 @@ final class InferredTypeProvider( if tpt.endPos.end > df.namePos.end then tpt.endPos.toLsp else df.namePos.endPos.toLsp + val spaceBefore = name.isOperatorName && paramss.isEmpty + adjustOpt.foreach(adjust => end.setEnd(adjust.adjustedEndPos)) new TextEdit( end, - ": " + printType(optDealias(tpt.typeOpt)) + printTypeAscription(optDealias(tpt.typeOpt), spaceBefore) ) end typeNameEdit @@ -220,8 +223,7 @@ final class InferredTypeProvider( while i >= 0 && sourceText(i) != ':' do i -= 1 i rhs match - case t: Tree[?] - if t.typeOpt.isErroneous && retryType && !tpt.sourcePos.span.isZeroExtent => + case t: Tree[?] if !tpt.sourcePos.span.isZeroExtent => inferredTypeEdits( Some( AdjustTypeOpts( @@ -239,9 +241,10 @@ final class InferredTypeProvider( */ case Some(bind @ Bind(name, body)) => def baseEdit(withParens: Boolean) = + val spaceBefore = name.isOperatorName new TextEdit( bind.endPos.toLsp, - ": " + printType(optDealias(body.typeOpt)) + { + printTypeAscription(optDealias(body.typeOpt), spaceBefore) + { if withParens then ")" else "" } ) @@ -272,9 +275,10 @@ final class InferredTypeProvider( * `for(t: Int <- 0 to 10)` */ case Some(i @ Ident(name)) => + val spaceBefore = name.isOperatorName val typeNameEdit = new TextEdit( i.endPos.toLsp, - ": " + printType(optDealias(i.typeOpt.widen)) + printTypeAscription(optDealias(i.typeOpt.widen), spaceBefore) ) typeNameEdit :: imports diff --git a/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala b/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala index 0e978049d177..60a884eb2934 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/MetalsInteractive.scala @@ -5,12 +5,16 @@ import scala.annotation.tailrec import dotc.* import ast.*, tpd.* +import dotty.tools.dotc.core.Constants.* import core.*, Contexts.*, Flags.*, Names.*, Symbols.*, Types.* +import dotty.tools.dotc.core.StdNames.* import interactive.* import util.* import util.SourcePosition +import dotty.tools.pc.utils.InteractiveEnrichments.* object MetalsInteractive: + type NamedTupleArg = String def contextOfStat( stats: List[Tree], @@ -96,9 +100,9 @@ object MetalsInteractive: pos: SourcePosition, indexed: IndexedContext, skipCheckOnName: Boolean = false - ): List[Symbol] = + )(using Context): List[Symbol] = enclosingSymbolsWithExpressionType(path, pos, indexed, skipCheckOnName) - .map(_._1) + .map(_._1.sourceSymbol) /** * Returns the list of tuple enclosing symbol and @@ -110,7 +114,7 @@ object MetalsInteractive: pos: SourcePosition, indexed: IndexedContext, skipCheckOnName: Boolean = false - ): List[(Symbol, Type)] = + ): List[(Symbol, Type, Option[String])] = import indexed.ctx path match // For a named arg, find the target `DefDef` and jump to the param @@ -118,59 +122,59 @@ object MetalsInteractive: val funSym = fn.symbol if funSym.is(Synthetic) && funSym.owner.is(CaseClass) then val sym = funSym.owner.info.member(name).symbol - List((sym, sym.info)) + List((sym, sym.info, None)) else val paramSymbol = for param <- funSym.paramSymss.flatten.find(_.name == name) yield param val sym = paramSymbol.getOrElse(fn.symbol) - List((sym, sym.info)) + List((sym, sym.info, None)) case (_: untpd.ImportSelector) :: (imp: Import) :: _ => importedSymbols(imp, _.span.contains(pos.span)).map(sym => - (sym, sym.info) + (sym, sym.info, None) ) - case (imp: Import) :: _ => + case (imp: ImportOrExport) :: _ => importedSymbols(imp, _.span.contains(pos.span)).map(sym => - (sym, sym.info) + (sym, sym.info, None) ) // wildcard param case head :: _ if (head.symbol.is(Param) && head.symbol.is(Synthetic)) => - List((head.symbol, head.typeOpt)) + List((head.symbol, head.typeOpt, None)) case (head @ Select(target, name)) :: _ if head.symbol.is(Synthetic) && name == StdNames.nme.apply => val sym = target.symbol if sym.is(Synthetic) && sym.is(Module) then - List((sym.companionClass, sym.companionClass.info)) - else List((target.symbol, target.typeOpt)) + List((sym.companionClass, sym.companionClass.info, None)) + else List((target.symbol, target.typeOpt, None)) // L@@ft(...) case (head @ ApplySelect(select)) :: _ if select.qualifier.sourcePos.contains(pos) && select.name == StdNames.nme.apply => - List((head.symbol, head.typeOpt)) + List((head.symbol, head.typeOpt, None)) // for Inlined we don't have a symbol, but it's needed to show proper type case (head @ Inlined(call, bindings, expansion)) :: _ => - List((call.symbol, head.typeOpt)) + List((call.symbol, head.typeOpt, None)) // for comprehension case (head @ ApplySelect(select)) :: _ if isForSynthetic(head) => // If the cursor is on the qualifier, return the symbol for it // `for { x <- List(1).head@@Option }` returns the symbol of `headOption` if select.qualifier.sourcePos.contains(pos) then - List((select.qualifier.symbol, select.qualifier.typeOpt)) + List((select.qualifier.symbol, select.qualifier.typeOpt, None)) // Otherwise, returns the symbol of for synthetics such as "withFilter" - else List((head.symbol, head.typeOpt)) + else List((head.symbol, head.typeOpt, None)) // f@@oo.bar case Select(target, _) :: _ if target.span.isSourceDerived && target.sourcePos.contains(pos) => - List((target.symbol, target.typeOpt)) + List((target.symbol, target.typeOpt, None)) /* In some cases type might be represented by TypeTree, however it's possible * that the type tree will not be marked properly as synthetic even if it doesn't @@ -185,7 +189,7 @@ object MetalsInteractive: */ case (tpt: TypeTree) :: parent :: _ if tpt.span != parent.span && !tpt.symbol.is(Synthetic) => - List((tpt.symbol, tpt.typeOpt)) + List((tpt.symbol, tpt.typeOpt, None)) /* TypeTest class https://dotty.epfl.ch/docs/reference/other-new-features/type-test.html * compiler automatically adds unapply if possible, we need to find the type symbol @@ -195,21 +199,23 @@ object MetalsInteractive: pat match case UnApply(fun, _, pats) => val tpeSym = pats.head.typeOpt.typeSymbol - List((tpeSym, tpeSym.info)) + List((tpeSym, tpeSym.info, None)) case _ => Nil + case head :: (sel @ Select(_, name)) :: _ + if head.sourcePos.encloses(sel.sourcePos) && (name == StdNames.nme.apply || name == StdNames.nme.unapply) => + val optObjectSymbol = List(head.symbol).filter(sym => !(sym.is(Synthetic) && sym.is(Module))) + val classSymbol = head.symbol.companionClass + val optApplySymbol = List(sel.symbol).filter(sym => !sym.is(Synthetic)) + val symbols = optObjectSymbol ++ (classSymbol :: optApplySymbol) + symbols.collect: + case sym if sym.exists => (sym, sym.info, None) + case path @ head :: tail => if head.symbol.is(Exported) then val sym = head.symbol.sourceSymbol - List((sym, sym.info)) - else if head.symbol.is(Synthetic) then - enclosingSymbolsWithExpressionType( - tail, - pos, - indexed, - skipCheckOnName - ) + List((sym, sym.info, None)) else if head.symbol != NoSymbol then if skipCheckOnName || MetalsInteractive.isOnName( @@ -217,7 +223,14 @@ object MetalsInteractive: pos, indexed.ctx.source ) - then List((head.symbol, head.typeOpt)) + then List((head.symbol, head.typeOpt, None)) + else if head.symbol.is(Synthetic) then + enclosingSymbolsWithExpressionType( + tail, + pos, + indexed, + skipCheckOnName + ) /* Type tree for List(1) has an Int type variable, which has span * but doesn't exist in code. * https://github.com/lampepfl/dotty/issues/15937 @@ -234,7 +247,7 @@ object MetalsInteractive: indexed, skipCheckOnName ) - else recovered.map(sym => (sym, sym.info)) + else recovered.map(sym => (sym, sym.info, None)) end if case Nil => Nil end match diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala b/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala index 1ebfd405768e..5ca5b63df0ae 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcCollector.scala @@ -63,7 +63,8 @@ trait PcCollector[T]: o.span.exists && o.span.point == named.symbol.owner.span.point ) - def soughtOrOverride(sym: Symbol) = + def soughtOrOverride(sym0: Symbol) = + val sym = if sym0.is(Flags.Exported) then sym0.sourceSymbol else sym0 sought(sym) || sym.allOverriddenSymbols.exists(sought(_)) def soughtTreeFilter(tree: Tree): Boolean = @@ -76,7 +77,7 @@ trait PcCollector[T]: case df: NamedDefTree if soughtOrOverride(df.symbol) && !df.symbol.isSetter => true - case imp: Import if owners(imp.expr.symbol) => true + case imp: ImportOrExport if owners(imp.expr.symbol) => true case _ => false def soughtFilter(f: Symbol => Boolean): Boolean = @@ -115,11 +116,13 @@ trait PcCollector[T]: */ case ident: Ident if ident.isCorrectSpan && filter(ident) => // symbols will differ for params in different ext methods, but source pos will be the same - if soughtFilter(_.sourcePos == ident.symbol.sourcePos) + val symbol = if ident.symbol.is(Flags.Exported) then ident.symbol.sourceSymbol else ident.symbol + if soughtFilter(_.sourcePos == symbol.sourcePos) then occurrences + collect( ident, - ident.sourcePos + ident.sourcePos, + Some(symbol) ) else occurrences /** @@ -160,7 +163,7 @@ trait PcCollector[T]: def collectEndMarker = EndMarker.getPosition(df, pos, sourceText).map: collect(EndMarker(df.symbol), _) - val annots = collectTrees(df.mods.annotations) + val annots = collectTrees(df.symbol.annotations.map(_.tree)) val traverser = new PcCollector.DeepFolderWithParent[Set[T]]( collectNamesWithParent @@ -215,8 +218,8 @@ trait PcCollector[T]: * @<>("") * def params() = ??? */ - case mdf: MemberDef if mdf.mods.annotations.nonEmpty => - val trees = collectTrees(mdf.mods.annotations) + case mdf: MemberDef if mdf.symbol.annotations.nonEmpty => + val trees = collectTrees(mdf.symbol.annotations.map(_.tree)) val traverser = new PcCollector.DeepFolderWithParent[Set[T]]( collectNamesWithParent @@ -228,7 +231,7 @@ trait PcCollector[T]: * For traversing import selectors: * import scala.util.<> */ - case imp: Import if filter(imp) => + case imp: ImportOrExport if filter(imp) => imp.selectors .collect { case sel: ImportSelector @@ -315,22 +318,25 @@ object EndMarker: def getPosition(df: NamedDefTree, pos: SourcePosition, sourceText: String)( implicit ct: Context ): Option[SourcePosition] = - val name = df.name.toString() - val endMarkerLine = - sourceText.slice(df.span.start, df.span.end).split('\n').last - val index = endMarkerLine.length() - name.length() - if index < 0 then None - else - val (possiblyEndMarker, possiblyEndMarkerName) = - endMarkerLine.splitAt(index) - Option.when( - possiblyEndMarkerName == name && - endMarkerRegex.matches(possiblyEndMarker) - )( - pos - .withStart(df.span.end - name.length()) - .withEnd(df.span.end) - ) + val name = df.name.toString().stripSuffix("$") + val lines = sourceText.slice(df.span.start, df.span.end).split('\n') + + if lines.nonEmpty then + val endMarkerLine = lines.last + val index = endMarkerLine.length() - name.length() + if index < 0 then None + else + val (possiblyEndMarker, possiblyEndMarkerName) = + endMarkerLine.splitAt(index) + Option.when( + possiblyEndMarkerName == name && + endMarkerRegex.matches(possiblyEndMarker) + )( + pos + .withStart(df.span.end - name.length()) + .withEnd(df.span.end) + ) + else None end getPosition end EndMarker diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcConvertToNamedLambdaParameters.scala b/presentation-compiler/src/main/dotty/tools/pc/PcConvertToNamedLambdaParameters.scala new file mode 100644 index 000000000000..5479a223bcce --- /dev/null +++ b/presentation-compiler/src/main/dotty/tools/pc/PcConvertToNamedLambdaParameters.scala @@ -0,0 +1,153 @@ +package dotty.tools.pc + +import java.nio.file.Paths +import java.util as ju + +import scala.jdk.CollectionConverters.* +import scala.meta.pc.OffsetParams + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.interactive.Interactive +import dotty.tools.dotc.interactive.InteractiveDriver +import dotty.tools.dotc.util.SourceFile +import dotty.tools.dotc.util.SourcePosition +import org.eclipse.lsp4j as l +import dotty.tools.pc.utils.InteractiveEnrichments.* +import dotty.tools.pc.utils.TermNameInference.* + +/** + * Facilitates the code action that converts a wildcard lambda to a lambda with named parameters + * e.g. + * + * List(1, 2).map(<<_>> + 1) => List(1, 2).map(i => i + 1) + */ +final class PcConvertToNamedLambdaParameters( + driver: InteractiveDriver, + params: OffsetParams +): + import PcConvertToNamedLambdaParameters._ + + def convertToNamedLambdaParameters: ju.List[l.TextEdit] = { + val uri = params.uri.nn + val filePath = Paths.get(uri) + driver.run( + uri, + SourceFile.virtual(filePath.toString, params.text.nn), + ) + given newctx: Context = driver.localContext(params) + val pos = driver.sourcePosition(params) + val trees = driver.openedTrees(uri) + val treeList = Interactive.pathTo(trees, pos) + // Extractor for a lambda function (needs context, so has to be defined here) + val LambdaExtractor = Lambda(using newctx) + // select the most inner wildcard lambda + val firstLambda = treeList.collectFirst { + case LambdaExtractor(params, rhsFn) if params.forall(isWildcardParam) => + params -> rhsFn + } + + firstLambda match { + case Some((params, lambda)) => + // avoid names that are either defined or referenced in the lambda + val namesToAvoid = allDefAndRefNamesInTree(lambda) + // compute parameter names based on the type of the parameter + val computedParamNames: List[String] = + params.foldLeft(List.empty[String]) { (acc, param) => + val name = singleLetterNameStream(param.tpe.typeSymbol.name.toString()) + .find(n => !namesToAvoid.contains(n) && !acc.contains(n)) + acc ++ name.toList + } + if computedParamNames.size == params.size then + val paramReferenceEdits = params.zip(computedParamNames).flatMap { (param, paramName) => + val paramReferencePosition = findParamReferencePosition(param, lambda) + paramReferencePosition.toList.map { pos => + val position = pos.toLsp + val range = new l.Range( + position.getStart(), + position.getEnd() + ) + new l.TextEdit(range, paramName) + } + } + val paramNamesStr = computedParamNames.mkString(", ") + val paramDefsStr = + if params.size == 1 then paramNamesStr + else s"($paramNamesStr)" + val defRange = new l.Range( + lambda.sourcePos.toLsp.getStart(), + lambda.sourcePos.toLsp.getStart() + ) + val paramDefinitionEdits = List( + new l.TextEdit(defRange, s"$paramDefsStr => ") + ) + (paramDefinitionEdits ++ paramReferenceEdits).asJava + else + List.empty.asJava + case _ => + List.empty.asJava + } + } + +end PcConvertToNamedLambdaParameters + +object PcConvertToNamedLambdaParameters: + val codeActionId = "ConvertToNamedLambdaParameters" + + class Lambda(using Context): + def unapply(tree: tpd.Block): Option[(List[tpd.ValDef], tpd.Tree)] = tree match { + case tpd.Block((ddef @ tpd.DefDef(_, tpd.ValDefs(params) :: Nil, _, body: tpd.Tree)) :: Nil, tpd.Closure(_, meth, _)) + if ddef.symbol == meth.symbol => + params match { + case List(param) => + // lambdas with multiple wildcard parameters are represented as a single parameter function and a block with wildcard valdefs + Some(multipleUnderscoresFromBody(param, body)) + case _ => Some(params -> body) + } + case _ => None + } + end Lambda + + private def multipleUnderscoresFromBody(param: tpd.ValDef, body: tpd.Tree)(using Context): (List[tpd.ValDef], tpd.Tree) = body match { + case tpd.Block(defs, expr) if param.symbol.is(Flags.Synthetic) => + val wildcardParamDefs = defs.collect { + case valdef: tpd.ValDef if isWildcardParam(valdef) => valdef + } + if wildcardParamDefs.size == defs.size then wildcardParamDefs -> expr + else List(param) -> body + case _ => List(param) -> body + } + + def isWildcardParam(param: tpd.ValDef)(using Context): Boolean = + param.name.toString.startsWith("_$") && param.symbol.is(Flags.Synthetic) + + def findParamReferencePosition(param: tpd.ValDef, lambda: tpd.Tree)(using Context): Option[SourcePosition] = + var pos: Option[SourcePosition] = None + object FindParamReference extends tpd.TreeTraverser: + override def traverse(tree: tpd.Tree)(using Context): Unit = + tree match + case ident @ tpd.Ident(_) if ident.symbol == param.symbol => + pos = Some(tree.sourcePos) + case _ => + traverseChildren(tree) + FindParamReference.traverse(lambda) + pos + end findParamReferencePosition + + def allDefAndRefNamesInTree(tree: tpd.Tree)(using Context): List[String] = + object FindDefinitionsAndRefs extends tpd.TreeAccumulator[List[String]]: + override def apply(x: List[String], tree: tpd.Tree)(using Context): List[String] = + tree match + case tpd.DefDef(name, _, _, _) => + super.foldOver(x :+ name.toString, tree) + case tpd.ValDef(name, _, _) => + super.foldOver(x :+ name.toString, tree) + case tpd.Ident(name) => + super.foldOver(x :+ name.toString, tree) + case _ => + super.foldOver(x, tree) + FindDefinitionsAndRefs.foldOver(Nil, tree) + end allDefAndRefNamesInTree + +end PcConvertToNamedLambdaParameters diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala index 3b2284bef1d0..69ec509043f8 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcDefinitionProvider.scala @@ -1,5 +1,6 @@ package dotty.tools.pc +import java.net.URI import java.nio.file.Paths import java.util.ArrayList @@ -16,6 +17,7 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags.{Exported, ModuleClass} import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.interactive.Interactive +import dotty.tools.dotc.interactive.Interactive.Include import dotty.tools.dotc.interactive.InteractiveDriver import dotty.tools.dotc.util.SourceFile import dotty.tools.dotc.util.SourcePosition @@ -49,12 +51,12 @@ class PcDefinitionProvider( Interactive.pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx) given ctx: Context = driver.localContext(params) - val indexedContext = IndexedContext(ctx) + val indexedContext = IndexedContext(pos)(using ctx) val result = - if findTypeDef then findTypeDefinitions(path, pos, indexedContext) - else findDefinitions(path, pos, indexedContext) + if findTypeDef then findTypeDefinitions(path, pos, indexedContext, uri) + else findDefinitions(path, pos, indexedContext, uri) - if result.locations().nn.isEmpty() then fallbackToUntyped(pos)(using ctx) + if result.locations().nn.isEmpty() then fallbackToUntyped(pos, uri)(using ctx) else result end definitions @@ -70,24 +72,26 @@ class PcDefinitionProvider( * @param pos cursor position * @return definition result */ - private def fallbackToUntyped(pos: SourcePosition)( + private def fallbackToUntyped(pos: SourcePosition, uri: URI)( using ctx: Context ) = lazy val untpdPath = NavigateAST .untypedPath(pos.span) .collect { case t: untpd.Tree => t } - definitionsForSymbol(untpdPath.headOption.map(_.symbol).toList, pos) + definitionsForSymbols(untpdPath.headOption.map(_.symbol).toList, uri, pos) end fallbackToUntyped private def findDefinitions( path: List[Tree], pos: SourcePosition, - indexed: IndexedContext + indexed: IndexedContext, + uri: URI, ): DefinitionResult = import indexed.ctx - definitionsForSymbol( + definitionsForSymbols( MetalsInteractive.enclosingSymbols(path, pos, indexed), + uri, pos ) end findDefinitions @@ -95,80 +99,72 @@ class PcDefinitionProvider( private def findTypeDefinitions( path: List[Tree], pos: SourcePosition, - indexed: IndexedContext + indexed: IndexedContext, + uri: URI, ): DefinitionResult = import indexed.ctx val enclosing = path.expandRangeToEnclosingApply(pos) val typeSymbols = MetalsInteractive .enclosingSymbolsWithExpressionType(enclosing, pos, indexed) - .map { case (_, tpe) => + .map { case (_, tpe, _) => tpe.typeSymbol } typeSymbols match case Nil => path.headOption match case Some(value: Literal) => - definitionsForSymbol(List(value.typeOpt.widen.typeSymbol), pos) + definitionsForSymbols(List(value.typeOpt.widen.typeSymbol), uri, pos) case _ => DefinitionResultImpl.empty case _ => - definitionsForSymbol(typeSymbols, pos) - + definitionsForSymbols(typeSymbols, uri, pos) end findTypeDefinitions - private def definitionsForSymbol( + private def definitionsForSymbols( symbols: List[Symbol], + uri: URI, pos: SourcePosition )(using ctx: Context): DefinitionResult = - symbols match - case symbols @ (sym :: other) => - val isLocal = sym.source == pos.source - if isLocal then - val (exportedDefs, otherDefs) = - Interactive.findDefinitions(List(sym), driver, false, false) - .filter(_.source == sym.source) - .partition(_.tree.symbol.is(Exported)) - - otherDefs.headOption.orElse(exportedDefs.headOption) match - case Some(srcTree) => - val pos = srcTree.namePos - if pos.exists then - val loc = new Location(params.uri().toString(), pos.toLsp) - DefinitionResultImpl( - SemanticdbSymbols.symbolName(sym), - List(loc).asJava, - ) - else DefinitionResultImpl.empty - case None => - DefinitionResultImpl.empty - else - val res = new ArrayList[Location]() - semanticSymbolsSorted(symbols) - .foreach { sym => - res.addAll(search.definition(sym, params.uri())) - } - DefinitionResultImpl( - SemanticdbSymbols.symbolName(sym), - res - ) - end if + semanticSymbolsSorted(symbols) match case Nil => DefinitionResultImpl.empty - end match - end definitionsForSymbol + case syms @ ((_, headSym) :: tail) => + val locations = syms.flatMap: + case (sym, semanticdbSymbol) => + locationsForSymbol(sym, semanticdbSymbol, uri, pos) + DefinitionResultImpl(headSym, locations.asJava) + + private def locationsForSymbol( + symbol: Symbol, + semanticdbSymbol: String, + uri: URI, + pos: SourcePosition + )(using ctx: Context): List[Location] = + val isLocal = symbol.source == pos.source + if isLocal then + val trees = driver.openedTrees(uri) + val include = Include.definitions | Include.local + val (exportedDefs, otherDefs) = + Interactive.findTreesMatching(trees, include, symbol) + .partition(_.tree.symbol.is(Exported)) + otherDefs.headOption.orElse(exportedDefs.headOption).collect: + case srcTree if srcTree.namePos.exists => + new Location(params.uri().toString(), srcTree.namePos.toLsp) + .toList + else search.definition(semanticdbSymbol, uri).nn.asScala.toList def semanticSymbolsSorted( syms: List[Symbol] - )(using ctx: Context): List[String] = + )(using ctx: Context): List[(Symbol, String)] = syms - .map { sym => + .collect { case sym if sym.exists => // in case of having the same type and teerm symbol // term comes first // used only for ordering symbols that come from `Import` val termFlag = if sym.is(ModuleClass) then sym.sourceModule.isTerm else sym.isTerm - (termFlag, SemanticdbSymbols.symbolName(sym)) + (termFlag, sym.sourceSymbol, SemanticdbSymbols.symbolName(sym)) } - .sorted - .map(_._2) + .sortBy { case (termFlag, _, name) => (termFlag, name) } + .map(_.tail) end PcDefinitionProvider diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala index 9c0e6bcfa9d8..40a351f2354d 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala @@ -5,7 +5,7 @@ import java.nio.file.Paths import scala.annotation.tailrec -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import dotty.tools.pc.utils.InteractiveEnrichments.* import dotty.tools.pc.printer.ShortenedTypePrinter import scala.meta.internal.pc.InlayHints @@ -17,6 +17,9 @@ import scala.meta.pc.SymbolSearch import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.NameOps.fieldName +import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.core.NameKinds.DefaultGetterName import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* @@ -51,13 +54,13 @@ class PcInlayHintsProvider( val pos = driver.sourcePosition(params) def provide(): List[InlayHint] = - val deepFolder = DeepFolder[InlayHints](collectDecorations) + val deepFolder = PcCollector.DeepFolderWithParent[InlayHints](collectDecorations) Interactive .pathTo(driver.openedTrees(uri), pos)(using driver.currentCtx) .headOption .getOrElse(unit.tpdTree) .enclosedChildren(pos.span) - .flatMap(tpdTree => deepFolder(InlayHints.empty, tpdTree).result()) + .flatMap(tpdTree => deepFolder(InlayHints.empty(params.uri().nn), tpdTree).result()) private def adjustPos(pos: SourcePosition): SourcePosition = pos.adjust(text)._1 @@ -65,11 +68,23 @@ class PcInlayHintsProvider( def collectDecorations( inlayHints: InlayHints, tree: Tree, + parent: Option[Tree] ): InlayHints = + // XRay hints are not mutually exclusive with other hints, so they must be matched separately + val firstPassHints = (tree, parent) match { + case XRayModeHint(tpe, pos) => + inlayHints.addToBlock( + adjustPos(pos).toLsp, + LabelPart(": ") :: toLabelParts(tpe, pos), + InlayHintKind.Type + ) + case _ => inlayHints + } + tree match case ImplicitConversion(symbol, range) => val adjusted = adjustPos(range) - inlayHints + firstPassHints .add( adjusted.startPos.toLsp, labelPart(symbol, symbol.decodedName) :: LabelPart("(") :: Nil, @@ -80,18 +95,18 @@ class PcInlayHintsProvider( LabelPart(")") :: Nil, InlayHintKind.Parameter, ) - case ImplicitParameters(symbols, pos, allImplicit) => - val labelParts = symbols.map(s => List(labelPart(s, s.decodedName))) - val label = - if allImplicit then labelParts.separated("(using ", ", ", ")") - else labelParts.separated(", ") - inlayHints.add( + case ImplicitParameters(trees, pos) => + firstPassHints.add( adjustPos(pos).toLsp, - label, + ImplicitParameters.partsFromImplicitArgs(trees).map((label, maybeSymbol) => + maybeSymbol match + case Some(symbol) => labelPart(symbol, label) + case None => LabelPart(label) + ), InlayHintKind.Parameter, ) case ValueOf(label, pos) => - inlayHints.add( + firstPassHints.add( adjustPos(pos).toLsp, LabelPart("(") :: LabelPart(label) :: List(LabelPart(")")), InlayHintKind.Parameter, @@ -99,7 +114,7 @@ class PcInlayHintsProvider( case TypeParameters(tpes, pos, sel) if !syntheticTupleApply(sel) => val label = tpes.map(toLabelParts(_, pos)).separated("[", ", ", "]") - inlayHints.add( + firstPassHints.add( adjustPos(pos).endPos.toLsp, label, InlayHintKind.Type, @@ -107,16 +122,55 @@ class PcInlayHintsProvider( case InferredType(tpe, pos, defTree) if !isErrorTpe(tpe) => val adjustedPos = adjustPos(pos).endPos - if inlayHints.containsDef(adjustedPos.start) then inlayHints + if firstPassHints.containsDef(adjustedPos.start) then firstPassHints else - inlayHints + firstPassHints .add( adjustedPos.toLsp, LabelPart(": ") :: toLabelParts(tpe, pos), InlayHintKind.Type, ) .addDefinition(adjustedPos.start) - case _ => inlayHints + case Parameters(isInfixFun, args) => + def isNamedParam(pos: SourcePosition): Boolean = + val start = text.indexWhere(!_.isWhitespace, pos.start) + val end = text.lastIndexWhere(!_.isWhitespace, pos.end - 1) + + text.slice(start, end).contains('=') + + def isBlockParam(pos: SourcePosition): Boolean = + val start = text.indexWhere(!_.isWhitespace, pos.start) + val end = text.lastIndexWhere(!_.isWhitespace, pos.end - 1) + val startsWithBrace = text.lift(start).contains('{') + val endsWithBrace = text.lift(end).contains('}') + + startsWithBrace && endsWithBrace + + def adjustBlockParamPos(pos: SourcePosition): SourcePosition = + pos.withStart(pos.start + 1) + + + args.foldLeft(firstPassHints) { + case (ih, (name, pos0, isByName)) => + val pos = adjustPos(pos0) + val isBlock = isBlockParam(pos) + val namedLabel = + if params.namedParameters() && !isInfixFun && !isBlock && !isNamedParam(pos) then s"${name} = " else "" + val byNameLabel = + if params.byNameParameters() && isByName && (!isInfixFun || isBlock) then "=> " else "" + + val labelStr = s"${namedLabel}${byNameLabel}" + val hintPos = if isBlock then adjustBlockParamPos(pos) else pos + + if labelStr.nonEmpty then + ih.add( + hintPos.startPos.toLsp, + List(LabelPart(labelStr)), + InlayHintKind.Parameter, + ) + else ih + } + case _ => firstPassHints private def toLabelParts( tpe: Type, @@ -125,7 +179,7 @@ class PcInlayHintsProvider( val tpdPath = Interactive.pathTo(unit.tpdTree, pos.span) - val indexedCtx = IndexedContext(Interactive.contextOfPath(tpdPath)) + val indexedCtx = IndexedContext(pos)(using Interactive.contextOfPath(tpdPath)) val printer = ShortenedTypePrinter( symbolSearch )(using indexedCtx) @@ -140,7 +194,7 @@ class PcInlayHintsProvider( isInScope(tycon) && args.forall(isInScope) case _ => true if isInScope(tpe) then tpe - else tpe.deepDealias(using indexedCtx.ctx) + else tpe.deepDealiasAndSimplify(using indexedCtx.ctx) val dealiased = optDealias(tpe) val tpeStr = printer.tpe(dealiased) @@ -149,7 +203,7 @@ class PcInlayHintsProvider( InlayHints.makeLabelParts(parts, tpeStr) end toLabelParts - private val definitions = IndexedContext(ctx).ctx.definitions + private val definitions = IndexedContext(pos)(using ctx).ctx.definitions private def syntheticTupleApply(tree: Tree): Boolean = tree match case sel: Select => @@ -221,12 +275,8 @@ object ImplicitParameters: case Apply(fun, args) if args.exists(isSyntheticArg) && !tree.sourcePos.span.isZeroExtent && !args.exists(isQuotes(_)) => val (implicitArgs, providedArgs) = args.partition(isSyntheticArg) - val allImplicit = providedArgs.isEmpty || providedArgs.forall { - case Ident(name) => name == nme.MISSING - case _ => false - } val pos = implicitArgs.head.sourcePos - Some(implicitArgs.map(_.symbol), pos, allImplicit) + Some(implicitArgs, pos) case _ => None } else None @@ -242,6 +292,67 @@ object ImplicitParameters: private def isQuotes(tree: Tree)(using Context) = tree.tpe.typeSymbol == defn.QuotesClass + def partsFromImplicitArgs(trees: List[Tree])(using Context): List[(String, Option[Symbol])] = { + @tailrec + def recurseImplicitArgs( + currentArgs: List[Tree], + remainingArgsLists: List[List[Tree]], + parts: List[(String, Option[Symbol])] + ): List[(String, Option[Symbol])] = + (currentArgs, remainingArgsLists) match { + case (Nil, Nil) => parts + case (Nil, headArgsList :: tailArgsList) => + if (headArgsList.isEmpty) { + recurseImplicitArgs( + headArgsList, + tailArgsList, + (")", None) :: parts + ) + } else { + recurseImplicitArgs( + headArgsList, + tailArgsList, + (", ", None) :: (")", None) :: parts + ) + } + case (arg :: remainingArgs, remainingArgsLists) => + arg match { + case Apply(fun, args) => + val applyLabel = (fun.symbol.decodedName, Some(fun.symbol)) + recurseImplicitArgs( + args, + remainingArgs :: remainingArgsLists, + ("(", None) :: applyLabel :: parts + ) + case t if t.isTerm => + val termLabel = (t.symbol.decodedName, Some(t.symbol)) + if (remainingArgs.isEmpty) + recurseImplicitArgs( + remainingArgs, + remainingArgsLists, + termLabel :: parts + ) + else + recurseImplicitArgs( + remainingArgs, + remainingArgsLists, + (", ", None) :: termLabel :: parts + ) + case _ => + recurseImplicitArgs( + remainingArgs, + remainingArgsLists, + parts + ) + } + } + ((")", None) :: recurseImplicitArgs( + trees, + Nil, + List(("(using ", None)) + )).reverse + } + end ImplicitParameters object ValueOf: @@ -331,3 +442,116 @@ object InferredType: index >= 0 && index < afterDef.size && afterDef(index) == '@' end InferredType + +object Parameters: + def unapply(tree: Tree)(using params: InlayHintsParams, ctx: Context): Option[(Boolean, List[(Name, SourcePosition, Boolean)])] = + def shouldSkipFun(fun: Tree)(using Context): Boolean = + fun match + case sel: Select => isForComprehensionMethod(sel) || sel.symbol.name == nme.unapply || sel.symbol.is(Flags.JavaDefined) + case _ => false + + def isInfixFun(fun: Tree, args: List[Tree])(using Context): Boolean = + val isInfixSelect = fun match + case Select(sel, _) => sel.isInfix + case _ => false + val source = fun.source + if args.isEmpty then isInfixSelect + else + (!(fun.span.end until args.head.span.start) + .map(source.apply) + .contains('.') && fun.symbol.is(Flags.ExtensionMethod)) || isInfixSelect + + def isRealApply(tree: Tree) = + !tree.symbol.isOneOf(Flags.GivenOrImplicit) && !tree.span.isZeroExtent + + def getUnderlyingFun(tree: Tree): Tree = + tree match + case Apply(fun, _) => getUnderlyingFun(fun) + case TypeApply(fun, _) => getUnderlyingFun(fun) + case t => t + + @tailrec + def isDefaultArg(arg: Tree): Boolean = arg match + case Ident(name) => name.is(DefaultGetterName) + case Select(_, name) => name.is(DefaultGetterName) + case Apply(fun, _) => isDefaultArg(fun) + case _ => false + + if (params.namedParameters() || params.byNameParameters()) then + tree match + case Apply(fun, args) if isRealApply(fun) => + val underlyingFun = getUnderlyingFun(fun) + if shouldSkipFun(underlyingFun) then + None + else + val funTp = fun.typeOpt.widenTermRefExpr + val paramNames = funTp.paramNamess.flatten + val paramInfos = funTp.paramInfoss.flatten + + Some( + isInfixFun(fun, args) || underlyingFun.isInfix, + ( + args + .zip(paramNames) + .zip(paramInfos) + .collect { + case ((arg, paramName), paramInfo) if !arg.span.isZeroExtent && !isDefaultArg(arg) => + (paramName.fieldName, arg.sourcePos, paramInfo.isByName) + } + ) + ) + case _ => None + else None +end Parameters + +object XRayModeHint: + def unapply(trees: (Tree, Option[Tree]))(using params: InlayHintsParams, ctx: Context): Option[(Type, SourcePosition)] = + if params.hintsXRayMode() then + val (tree, parent) = trees + val isParentApply = parent match + case Some(_: Apply) => true + case _ => false + val isParentOnSameLine = parent match + case Some(sel: Select) if sel.isForComprehensionMethod => false + case Some(par) if par.sourcePos.exists && par.sourcePos.line == tree.sourcePos.line => true + case _ => false + + tree match + /* + anotherTree + .innerSelect() + */ + case a @ Apply(inner, _) + if inner.sourcePos.exists && !isParentOnSameLine && !isParentApply && + endsInSimpleSelect(a) && isEndOfLine(tree.sourcePos) => + Some((a.tpe.widen.deepDealiasAndSimplify, tree.sourcePos)) + /* + innerTree + .select + */ + case select @ Select(innerTree, _) + if innerTree.sourcePos.exists && !isParentOnSameLine && !isParentApply && + isEndOfLine(tree.sourcePos) => + Some((select.tpe.widen.deepDealiasAndSimplify, tree.sourcePos)) + case _ => None + else None + + @tailrec + private def endsInSimpleSelect(ap: Tree)(using ctx: Context): Boolean = + ap match + case Apply(sel: Select, _) => + sel.name != nme.apply && !isInfix(sel) + case Apply(TypeApply(sel: Select, _), _) => + sel.name != nme.apply && !isInfix(sel) + case Apply(innerTree @ Apply(_, _), _) => + endsInSimpleSelect(innerTree) + case _ => false + + private def isEndOfLine(pos: SourcePosition): Boolean = + if pos.exists then + val source = pos.source + val end = pos.end + end >= source.length || source(end) == '\n' || source(end) == '\r' + else false + +end XRayModeHint diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProviderImpl.scala b/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProvider.scala similarity index 54% rename from presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProviderImpl.scala rename to presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProvider.scala index bbba44d0d84f..5679568d620b 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProviderImpl.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcInlineValueProvider.scala @@ -16,16 +16,76 @@ import dotty.tools.dotc.core.StdNames import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver +import dotty.tools.dotc.util.SourceFile import dotty.tools.dotc.util.SourcePosition import dotty.tools.pc.utils.InteractiveEnrichments.* +import dotty.tools.pc.IndexedContext.Result import org.eclipse.lsp4j as l -final class PcInlineValueProviderImpl( +final class PcInlineValueProvider( driver: InteractiveDriver, val params: OffsetParams -) extends WithSymbolSearchCollector[Option[Occurence]](driver, params) - with InlineValueProvider: +) extends WithSymbolSearchCollector[Option[Occurence]](driver, params): + + // We return a result or an error + def getInlineTextEdits(): Either[String, List[l.TextEdit]] = + defAndRefs() match { + case Right((defn, refs)) => + val edits = + if (defn.shouldBeRemoved) { + val defEdit = definitionTextEdit(defn) + val refsEdits = refs.map(referenceTextEdit(defn)) + defEdit :: refsEdits + } else refs.map(referenceTextEdit(defn)) + Right(edits) + case Left(error) => Left(error) + } + + private def referenceTextEdit( + definition: Definition + )(ref: Reference): l.TextEdit = + if (definition.requiresBrackets && ref.requiresBrackets) + new l.TextEdit( + ref.range, + s"""(${ref.rhs})""" + ) + else new l.TextEdit(ref.range, ref.rhs) + + private def definitionTextEdit(definition: Definition): l.TextEdit = + new l.TextEdit( + extend( + definition.rangeOffsets.start, + definition.rangeOffsets.end, + definition.range + ), + "" + ) + + private def extend( + startOffset: Int, + endOffset: Int, + range: l.Range + ): l.Range = { + val (startWithSpace, endWithSpace): (Int, Int) = + extendRangeToIncludeWhiteCharsAndTheFollowingNewLine( + text + )(startOffset, endOffset) + val startPos = new l.Position( + range.getStart.nn.getLine, + range.getStart.nn.getCharacter - (startOffset - startWithSpace) + ) + val endPos = + if (endWithSpace - 1 >= 0 && text(endWithSpace - 1) == '\n') + new l.Position(range.getEnd.nn.getLine + 1, 0) + else + new l.Position( + range.getEnd.nn.getLine, + range.getEnd.nn.getCharacter + endWithSpace - endOffset + ) + + new l.Range(startPos, endPos) + } val position: l.Position = pos.toLsp.getStart().nn @@ -40,7 +100,7 @@ final class PcInlineValueProviderImpl( Some(Occurence(tree, parent, adjustedPos)) case _ => None - override def defAndRefs(): Either[String, (Definition, List[Reference])] = + def defAndRefs(): Either[String, (Definition, List[Reference])] = val newctx = driver.currentCtx.fresh.setCompilationUnit(unit) val allOccurences = result().flatten for @@ -49,7 +109,9 @@ final class PcInlineValueProviderImpl( DefinitionTree(defn, pos) } .toRight(Errors.didNotFindDefinition) - symbols = symbolsUsedInDefn(definition.tree.rhs) + path = Interactive.pathTo(unit.tpdTree, definition.tree.rhs.span)(using newctx) + indexedContext = IndexedContext(definition.tree.namePos)(using Interactive.contextOfPath(path)(using newctx)) + symbols = symbolsUsedInDefn(definition.tree.rhs, indexedContext) references <- getReferencesToInline(definition, allOccurences, symbols) yield val (deleteDefinition, refsEdits) = references @@ -57,7 +119,6 @@ final class PcInlineValueProviderImpl( val defPos = definition.tree.sourcePos val defEdit = Definition( defPos.toLsp, - adjustRhs(definition.tree.rhs.sourcePos), RangeOffset(defPos.start, defPos.end), definitionRequiresBrackets(definition.tree.rhs)(using newctx), deleteDefinition @@ -67,6 +128,15 @@ final class PcInlineValueProviderImpl( end for end defAndRefs + private def stripIndentPrefix(rhs: String, refIndent: String, defIndent: String, hasNextLineAfterEqualsSign: Boolean): String = + val rhsLines = rhs.split("\n").nn.toList + rhsLines match + case h :: Nil => rhs + case h :: t => + val header = if !hasNextLineAfterEqualsSign then h else s"\n$refIndent $h" + header.nn ++ t.map(refIndent ++ _.nn.stripPrefix(defIndent)).mkString("\n", "\n", "") + case Nil => rhs + private def definitionRequiresBrackets(tree: Tree)(using Context): Boolean = NavigateAST .untypedPath(tree.span) @@ -99,46 +169,48 @@ final class PcInlineValueProviderImpl( end referenceRequiresBrackets - private def adjustRhs(pos: SourcePosition) = + private def extendWithSurroundingParens(pos: SourcePosition) = + /** Move `point` by `step` as long as the character at `point` is `acceptedChar` */ def extend(point: Int, acceptedChar: Char, step: Int): Int = val newPoint = point + step - if newPoint > 0 && newPoint < text.length && text( - newPoint - ) == acceptedChar + if newPoint > 0 && newPoint < text.length && + text(newPoint) == acceptedChar then extend(newPoint, acceptedChar, step) else point val adjustedStart = extend(pos.start, '(', -1) val adjustedEnd = extend(pos.end - 1, ')', 1) + 1 text.slice(adjustedStart, adjustedEnd).mkString - private def symbolsUsedInDefn( - rhs: Tree - ): List[Symbol] = + private def symbolsUsedInDefn(rhs: Tree, indexedContext: IndexedContext): Set[Symbol] = def collectNames( - symbols: List[Symbol], + symbols: Set[Symbol], tree: Tree - ): List[Symbol] = + ): Set[Symbol] = tree match - case id: (Ident | Select) + case id: Ident if !id.symbol.is(Synthetic) && !id.symbol.is(Implicit) => - tree.symbol :: symbols + symbols + tree.symbol + case sel: Select => + indexedContext.lookupSym(sel.symbol) match + case IndexedContext.Result.InScope => symbols + sel.symbol + case _ => symbols case _ => symbols - val traverser = new DeepFolder[List[Symbol]](collectNames) - traverser(List(), rhs) + val traverser = new DeepFolder[Set[Symbol]](collectNames) + traverser(Set(), rhs) end symbolsUsedInDefn private def getReferencesToInline( definition: DefinitionTree, allOccurences: List[Occurence], - symbols: List[Symbol] + symbols: Set[Symbol] ): Either[String, (Boolean, List[Reference])] = val defIsLocal = definition.tree.symbol.ownersIterator .drop(1) .exists(e => e.isTerm) def allreferences = allOccurences.filterNot(_.isDefn) def inlineAll() = - makeRefsEdits(allreferences, symbols).map((true, _)) + makeRefsEdits(allreferences, symbols, definition).map((true, _)) if definition.tree.sourcePos.toLsp.encloses(position) then if defIsLocal then inlineAll() else Left(Errors.notLocal) else @@ -149,21 +221,35 @@ final class PcInlineValueProviderImpl( ref <- list .find(_.pos.toLsp.encloses(position)) .toRight(Errors.didNotFindReference) - refEdits <- makeRefsEdits(List(ref), symbols) + refEdits <- makeRefsEdits(List(ref), symbols, definition) yield (false, refEdits) end if end getReferencesToInline + extension (pos: SourcePosition) + def startColumnIndentPadding: String = { + val source = pos.source + val offset = pos.start + var idx = source.startOfLine(offset) + val pad = new StringBuilder + while (idx != offset && idx < source.content().length && source.content()(idx).isWhitespace) { + pad.append(source.content()(idx)) + idx += 1 + } + pad.result() + } + private def makeRefsEdits( refs: List[Occurence], - symbols: List[Symbol] + symbols: Set[Symbol], + definition: DefinitionTree ): Either[String, List[Reference]] = val newctx = driver.currentCtx.fresh.setCompilationUnit(unit) def buildRef(occurrence: Occurence): Either[String, Reference] = val path = Interactive.pathTo(unit.tpdTree, occurrence.pos.span)(using newctx) - val indexedContext = IndexedContext( - Interactive.contextOfPath(path)(using newctx) + val indexedContext = IndexedContext(pos)( + using Interactive.contextOfPath(path)(using newctx) ) import indexedContext.ctx val conflictingSymbols = symbols @@ -173,10 +259,18 @@ final class PcInlineValueProviderImpl( case _ => false } .map(_.fullNameBackticked) + val hasNextLineAfterEqualsSign = + definition.tree.sourcePos.startLine != definition.tree.rhs.sourcePos.startLine if conflictingSymbols.isEmpty then Right( Reference( occurrence.pos.toLsp, + stripIndentPrefix( + extendWithSurroundingParens(definition.tree.rhs.sourcePos), + occurrence.tree.startPos.startColumnIndentPadding, + definition.tree.startPos.startColumnIndentPadding, + hasNextLineAfterEqualsSign + ), occurrence.parent.map(p => RangeOffset(p.sourcePos.start, p.sourcePos.end) ), @@ -195,7 +289,7 @@ final class PcInlineValueProviderImpl( ) end makeRefsEdits -end PcInlineValueProviderImpl +end PcInlineValueProvider case class Occurence(tree: Tree, parent: Option[Tree], pos: SourcePosition): def isDefn = @@ -204,3 +298,19 @@ case class Occurence(tree: Tree, parent: Option[Tree], pos: SourcePosition): case _ => false case class DefinitionTree(tree: ValDef, pos: SourcePosition) + +case class RangeOffset(start: Int, end: Int) + +case class Definition( + range: l.Range, + rangeOffsets: RangeOffset, + requiresBrackets: Boolean, + shouldBeRemoved: Boolean +) + +case class Reference( + range: l.Range, + rhs: String, + parentOffsets: Option[RangeOffset], + requiresBrackets: Boolean +) diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcRenameProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/PcRenameProvider.scala index 666ccf9c614f..467f331ea7dc 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcRenameProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcRenameProvider.scala @@ -18,11 +18,19 @@ final class PcRenameProvider( name: Option[String] ) extends WithSymbolSearchCollector[l.TextEdit](driver, params): private val forbiddenMethods = - Set("equals", "hashCode", "unapply", "unary_!", "!") + Set("equals", "hashCode", "unapply", "apply", "", "unary_!", "!") + + private val soughtSymbolNames = soughtSymbols match + case Some((symbols, _)) => + symbols.filterNot(_.isError).map(symbol => symbol.decodedName.toString) + case None => Set.empty[String] + def canRenameSymbol(sym: Symbol)(using Context): Boolean = - (!sym.is(Method) || !forbiddenMethods(sym.decodedName)) - && (sym.ownersIterator.drop(1).exists(ow => ow.is(Method)) - || sym.source.path.isWorksheet) + val decodedName = sym.decodedName + def isForbiddenMethod = sym.is(Method) && forbiddenMethods(decodedName) + def local = sym.ownersIterator.drop(1).exists(ow => ow.is(Method)) + def isInWorksheet = sym.source.path.isWorksheet + !isForbiddenMethod && (local || isInWorksheet) && soughtSymbolNames(decodedName) def prepareRename(): Option[l.Range] = soughtSymbols.flatMap((symbols, pos) => diff --git a/presentation-compiler/src/main/dotty/tools/pc/PcSymbolSearch.scala b/presentation-compiler/src/main/dotty/tools/pc/PcSymbolSearch.scala index fd3d74f16c16..7d1e53e1ddb2 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/PcSymbolSearch.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/PcSymbolSearch.scala @@ -49,7 +49,7 @@ trait PcSymbolSearch: lazy val soughtSymbols: Option[(Set[Symbol], SourcePosition)] = soughtSymbols(path) - def soughtSymbols(path: List[Tree]): Option[(Set[Symbol], SourcePosition)] = + private def soughtSymbols(path: List[Tree]): Option[(Set[Symbol], SourcePosition)] = val sought = path match /* reference of an extension paramter * extension [EF](<>: List[EF]) @@ -148,7 +148,7 @@ trait PcSymbolSearch: /* Import selectors: * import scala.util.Tr@@y */ - case (imp: Import) :: _ if imp.span.contains(pos.span) => + case (imp: ImportOrExport) :: _ if imp.span.contains(pos.span) => imp .selector(pos.span) .map(sym => (symbolAlternatives(sym), sym.sourcePos)) diff --git a/presentation-compiler/src/main/dotty/tools/pc/Scala3CompilerAccess.scala b/presentation-compiler/src/main/dotty/tools/pc/Scala3CompilerAccess.scala index 1443fbcf37cc..f6fc48e5ae67 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/Scala3CompilerAccess.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/Scala3CompilerAccess.scala @@ -3,7 +3,7 @@ package dotty.tools.pc import java.util.concurrent.ScheduledExecutorService import scala.concurrent.ExecutionContextExecutor -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.pc.CompilerAccess import scala.meta.pc.PresentationCompilerConfig diff --git a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala index 218d92c38ffa..18311d1b7853 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala @@ -15,10 +15,10 @@ import scala.jdk.CollectionConverters._ import scala.language.unsafeNulls import scala.meta.internal.metals.CompilerVirtualFileParams import scala.meta.internal.metals.EmptyCancelToken -import scala.meta.internal.metals.EmptyReportContext -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.EmptyReportContext +import scala.meta.internal.metals.PcQueryContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.metals.ReportLevel -import scala.meta.internal.metals.StdReportContext import scala.meta.internal.mtags.CommonMtagsEnrichments.* import scala.meta.internal.pc.CompilerAccess import scala.meta.internal.pc.DefinitionResultImpl @@ -53,8 +53,21 @@ case class ScalaPresentationCompiler( folderPath: Option[Path] = None, reportsLevel: ReportLevel = ReportLevel.Info, completionItemPriority: CompletionItemPriority = (_: String) => 0, + reportContext: ReportContext = EmptyReportContext() ) extends PresentationCompiler: + given ReportContext = reportContext + + override def supportedCodeActions(): ju.List[String] = List( + CodeActionId.ConvertToNamedArguments, + CodeActionId.ImplementAbstractMembers, + CodeActionId.ExtractMethod, + CodeActionId.InlineValue, + CodeActionId.InsertInferredType, + CodeActionId.InsertInferredMethod, + PcConvertToNamedLambdaParameters.codeActionId + ).asJava + def this() = this("", None, Nil, Nil) val scalaVersion = BuildInfo.scalaVersion @@ -62,10 +75,44 @@ case class ScalaPresentationCompiler( private val forbiddenOptions = Set("-print-lines", "-print-tasty") private val forbiddenDoubleOptions = Set.empty[String] - given ReportContext = - folderPath - .map(StdReportContext(_, _ => buildTargetName, reportsLevel)) - .getOrElse(EmptyReportContext) + + override def codeAction[T]( + params: OffsetParams, + codeActionId: String, + codeActionPayload: Optional[T] + ): CompletableFuture[ju.List[TextEdit]] = + (codeActionId, codeActionPayload.asScala) match + case ( + CodeActionId.ConvertToNamedArguments, + Some(argIndices: ju.List[_]) + ) => + val payload = + argIndices.asScala.collect { case i: Integer => i.toInt }.toSet + convertToNamedArguments(params, payload) + case (CodeActionId.ImplementAbstractMembers, _) => + implementAbstractMembers(params) + case (CodeActionId.InsertInferredType, _) => + insertInferredType(params) + case (CodeActionId.InsertInferredMethod, _) => + insertInferredMethod(params) + case (CodeActionId.InlineValue, _) => + inlineValue(params) + case (CodeActionId.ExtractMethod, Some(extractionPos: OffsetParams)) => + params match { + case range: RangeParams => + extractMethod(range, extractionPos) + case _ => failedFuture(new IllegalArgumentException(s"Expected range parameters")) + } + case (PcConvertToNamedLambdaParameters.codeActionId, _) => + compilerAccess.withNonInterruptableCompiler(List.empty[l.TextEdit].asJava, params.token) { + access => PcConvertToNamedLambdaParameters(access.compiler(), params).convertToNamedLambdaParameters + }(params.toQueryContext) + case (id, _) => failedFuture(new IllegalArgumentException(s"Unsupported action id $id")) + + private def failedFuture[T](e: Throwable): CompletableFuture[T] = + val f = new CompletableFuture[T]() + f.completeExceptionally(e) + f override def withCompletionItemPriority( priority: CompletionItemPriority @@ -103,18 +150,18 @@ case class ScalaPresentationCompiler( override def semanticTokens( params: VirtualFileParams ): CompletableFuture[ju.List[Node]] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( new ju.ArrayList[Node](), params.token() ) { access => val driver = access.compiler() new PcSemanticTokensProvider(driver, params).provide().asJava - } + }(params.toQueryContext) override def inlayHints( params: InlayHintsParams ): ju.concurrent.CompletableFuture[ju.List[l.InlayHint]] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( new ju.ArrayList[l.InlayHint](), params.token(), ) { access => @@ -122,7 +169,7 @@ case class ScalaPresentationCompiler( new PcInlayHintsProvider(driver, params, search) .provide() .asJava - } + }(params.toQueryContext) override def getTasty( targetUri: URI, @@ -133,7 +180,7 @@ case class ScalaPresentationCompiler( } def complete(params: OffsetParams): CompletableFuture[l.CompletionList] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( EmptyCompletionList(), params.token() ) { access => @@ -148,44 +195,43 @@ case class ScalaPresentationCompiler( folderPath, completionItemPriority ).completions() - - } + }(params.toQueryContext) def definition(params: OffsetParams): CompletableFuture[DefinitionResult] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( DefinitionResultImpl.empty, params.token() ) { access => val driver = access.compiler() PcDefinitionProvider(driver, params, search).definitions() - } + }(params.toQueryContext) override def typeDefinition( params: OffsetParams ): CompletableFuture[DefinitionResult] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( DefinitionResultImpl.empty, params.token() ) { access => val driver = access.compiler() PcDefinitionProvider(driver, params, search).typeDefinitions() - } + }(params.toQueryContext) def documentHighlight( params: OffsetParams ): CompletableFuture[ju.List[DocumentHighlight]] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( List.empty[DocumentHighlight].asJava, params.token() ) { access => val driver = access.compiler() PcDocumentHighlightProvider(driver, params).highlights.asJava - } + }(params.toQueryContext) override def references( params: ReferencesRequest ): CompletableFuture[ju.List[ReferencesResult]] = - compilerAccess.withNonInterruptableCompiler(Some(params.file()))( + compilerAccess.withNonInterruptableCompiler( List.empty[ReferencesResult].asJava, params.file().token, ) { access => @@ -193,16 +239,16 @@ case class ScalaPresentationCompiler( PcReferencesProvider(driver, params) .references() .asJava - } + }(params.file().toQueryContext) def inferExpectedType(params: OffsetParams): CompletableFuture[ju.Optional[String]] = - compilerAccess.withInterruptableCompiler(Some(params))( + compilerAccess.withInterruptableCompiler( Optional.empty(), params.token, ) { access => val driver = access.compiler() new InferExpectedType(search, driver, params).infer().asJava - } + }(params.toQueryContext) def shutdown(): Unit = compilerAccess.shutdown() @@ -217,8 +263,6 @@ case class ScalaPresentationCompiler( symbol: String ): CompletableFuture[Optional[IPcSymbolInformation]] = compilerAccess.withNonInterruptableCompiler[Optional[IPcSymbolInformation]]( - None - )( Optional.empty(), EmptyCancelToken, ) { access => @@ -226,27 +270,27 @@ case class ScalaPresentationCompiler( .info(symbol) .map(_.asJava) .asJava - } + }(emptyQueryContext) def semanticdbTextDocument( filename: URI, code: String ): CompletableFuture[Array[Byte]] = val virtualFile = CompilerVirtualFileParams(filename, code) - compilerAccess.withNonInterruptableCompiler(Some(virtualFile))( + compilerAccess.withNonInterruptableCompiler( Array.empty[Byte], EmptyCancelToken ) { access => val driver = access.compiler() val provider = SemanticdbTextDocumentProvider(driver, folderPath) provider.textDocument(filename, code) - } + }(virtualFile.toQueryContext) def completionItemResolve( item: l.CompletionItem, symbol: String ): CompletableFuture[l.CompletionItem] = - compilerAccess.withNonInterruptableCompiler(None)( + compilerAccess.withNonInterruptableCompiler( item, EmptyCancelToken ) { access => @@ -254,7 +298,7 @@ case class ScalaPresentationCompiler( CompletionItemResolver.resolve(item, symbol, search, config)(using driver.currentCtx ) - } + }(emptyQueryContext) def autoImports( name: String, @@ -263,7 +307,7 @@ case class ScalaPresentationCompiler( ): CompletableFuture[ ju.List[scala.meta.pc.AutoImportsResult] ] = - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( List.empty[scala.meta.pc.AutoImportsResult].asJava, params.token() ) { access => @@ -278,13 +322,13 @@ case class ScalaPresentationCompiler( ) .autoImports(isExtension) .asJava - } + }(params.toQueryContext) def implementAbstractMembers( params: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( empty, params.token() ) { pc => @@ -295,31 +339,44 @@ case class ScalaPresentationCompiler( search, config ) - } + }(params.toQueryContext) end implementAbstractMembers override def insertInferredType( params: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( empty, params.token() ) { pc => new InferredTypeProvider(params, pc.compiler(), config, search) .inferredTypeEdits() .asJava - } + }(params.toQueryContext) + + def insertInferredMethod( + params: OffsetParams + ): CompletableFuture[ju.List[l.TextEdit]] = + val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() + compilerAccess.withNonInterruptableCompiler( + empty, + params.token() + ) { pc => + new InferredMethodProvider(params, pc.compiler(), config, search) + .inferredMethodEdits() + .asJava + }(params.toQueryContext) override def inlineValue( params: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = val empty: Either[String, List[l.TextEdit]] = Right(List()) (compilerAccess - .withInterruptableCompiler(Some(params))(empty, params.token()) { pc => - new PcInlineValueProviderImpl(pc.compiler(), params) + .withInterruptableCompiler(empty, params.token()) { pc => + new PcInlineValueProvider(pc.compiler(), params) .getInlineTextEdits() - }) + }(params.toQueryContext)) .thenApply { case Right(edits: List[TextEdit]) => edits.asJava case Left(error: String) => throw new DisplayableException(error) @@ -331,7 +388,7 @@ case class ScalaPresentationCompiler( extractionPos: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() - compilerAccess.withInterruptableCompiler(Some(range))(empty, range.token()) { + compilerAccess.withInterruptableCompiler(empty, range.token()) { pc => new ExtractMethodProvider( range, @@ -342,22 +399,28 @@ case class ScalaPresentationCompiler( ) .extractMethod() .asJava - } + }(range.toQueryContext) end extractMethod override def convertToNamedArguments( params: OffsetParams, argIndices: ju.List[Integer] + ): CompletableFuture[ju.List[l.TextEdit]] = + convertToNamedArguments(params, argIndices.asScala.toSet.map(_.toInt)) + + def convertToNamedArguments( + params: OffsetParams, + argIndices: Set[Int] ): CompletableFuture[ju.List[l.TextEdit]] = val empty: Either[String, List[l.TextEdit]] = Right(List()) (compilerAccess - .withNonInterruptableCompiler(Some(params))(empty, params.token()) { pc => + .withNonInterruptableCompiler(empty, params.token()) { pc => new ConvertToNamedArgumentsProvider( pc.compiler(), params, - argIndices.asScala.map(_.toInt).toSet + argIndices ).convertToNamedArguments - }) + }(params.toQueryContext)) .thenApplyAsync { case Left(error: String) => throw new DisplayableException(error) case Right(edits: List[l.TextEdit]) => edits.asJava @@ -367,33 +430,33 @@ case class ScalaPresentationCompiler( params: ju.List[OffsetParams] ): CompletableFuture[ju.List[l.SelectionRange]] = CompletableFuture.completedFuture { - compilerAccess.withSharedCompiler(params.asScala.headOption)( + compilerAccess.withSharedCompiler( List.empty[l.SelectionRange].asJava ) { pc => new SelectionRangeProvider( pc.compiler(), params, ).selectionRange().asJava - } + }(params.asScala.headOption.map(_.toQueryContext).getOrElse(emptyQueryContext)) } end selectionRange def hover( params: OffsetParams ): CompletableFuture[ju.Optional[HoverSignature]] = - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( ju.Optional.empty[HoverSignature](), params.token() ) { access => val driver = access.compiler() HoverProvider.hover(params, driver, search, config.hoverContentType()) - } + }(params.toQueryContext) end hover def prepareRename( params: OffsetParams ): CompletableFuture[ju.Optional[l.Range]] = - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( Optional.empty[l.Range](), params.token() ) { access => @@ -401,19 +464,19 @@ case class ScalaPresentationCompiler( Optional.ofNullable( PcRenameProvider(driver, params, None).prepareRename().orNull ) - } + }(params.toQueryContext) def rename( params: OffsetParams, name: String ): CompletableFuture[ju.List[l.TextEdit]] = - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( List[l.TextEdit]().asJava, params.token() ) { access => val driver = access.compiler() PcRenameProvider(driver, params, Some(name)).rename().asJava - } + }(params.toQueryContext) def newInstance( buildTargetIdentifier: String, @@ -427,13 +490,13 @@ case class ScalaPresentationCompiler( ) def signatureHelp(params: OffsetParams): CompletableFuture[l.SignatureHelp] = - compilerAccess.withNonInterruptableCompiler(Some(params))( + compilerAccess.withNonInterruptableCompiler( new l.SignatureHelp(), params.token() ) { access => val driver = access.compiler() SignatureHelpProvider.signatureHelp(driver, params, search) - } + }(params.toQueryContext) override def didChange( params: VirtualFileParams @@ -441,10 +504,10 @@ case class ScalaPresentationCompiler( CompletableFuture.completedFuture(Nil.asJava) override def didClose(uri: URI): Unit = - compilerAccess.withNonInterruptableCompiler(None)( + compilerAccess.withNonInterruptableCompiler( (), EmptyCancelToken - ) { access => access.compiler().close(uri) } + ) { access => access.compiler().close(uri) }(emptyQueryContext) override def withExecutorService( executorService: ExecutorService @@ -464,9 +527,27 @@ case class ScalaPresentationCompiler( def withSearch(search: SymbolSearch): PresentationCompiler = copy(search = search) + override def withReportContext(reportContext: ReportContext): PresentationCompiler = + copy(reportContext = reportContext) + def withWorkspace(workspace: Path): PresentationCompiler = copy(folderPath = Some(workspace)) override def isLoaded() = compilerAccess.isLoaded() + def additionalReportData() = + s"""|Scala version: $scalaVersion + |Classpath: + |${classpath + .map(path => s"$path [${if path.exists then "exists" else "missing"} ]") + .mkString(", ")} + |Options: + |${options.mkString(" ")} + |""".stripMargin + + extension (params: VirtualFileParams) + def toQueryContext = PcQueryContext(Some(params), additionalReportData) + + def emptyQueryContext = PcQueryContext(None, additionalReportData) + end ScalaPresentationCompiler diff --git a/presentation-compiler/src/main/dotty/tools/pc/SelectionRangeProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/SelectionRangeProvider.scala index 7973f4103ff6..09c44b105555 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/SelectionRangeProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/SelectionRangeProvider.scala @@ -6,7 +6,8 @@ import java.util as ju import scala.jdk.CollectionConverters._ import scala.meta.pc.OffsetParams -import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.untpd.* +import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver @@ -23,10 +24,7 @@ import org.eclipse.lsp4j.SelectionRange * @param compiler Metals Global presentation compiler wrapper. * @param params offset params converted from the selectionRange params. */ -class SelectionRangeProvider( - driver: InteractiveDriver, - params: ju.List[OffsetParams] -): +class SelectionRangeProvider(driver: InteractiveDriver, params: ju.List[OffsetParams]): /** * Get the seletion ranges for the provider params @@ -44,10 +42,13 @@ class SelectionRangeProvider( val source = SourceFile.virtual(filePath.toString, text) driver.run(uri, source) val pos = driver.sourcePosition(param) - val path = - Interactive.pathTo(driver.openedTrees(uri), pos)(using ctx) + val unit = driver.compilationUnits(uri) - val bareRanges = path + val untpdPath: List[Tree] = NavigateAST + .pathTo(pos.span, List(unit.untpdTree), true).collect: + case untpdTree: Tree => untpdTree + + val bareRanges = untpdPath .flatMap(selectionRangesFromTree(pos)) val comments = @@ -78,31 +79,43 @@ class SelectionRangeProvider( end selectionRange /** Given a tree, create a seq of [[SelectionRange]]s corresponding to that tree. */ - private def selectionRangesFromTree(pos: SourcePosition)(tree: tpd.Tree)(using Context) = + private def selectionRangesFromTree(pos: SourcePosition)(tree: Tree)(using Context) = def toSelectionRange(srcPos: SourcePosition) = val selectionRange = new SelectionRange() selectionRange.setRange(srcPos.toLsp) selectionRange - val treeSelectionRange = toSelectionRange(tree.sourcePos) - - tree match - case tpd.DefDef(name, paramss, tpt, rhs) => - // If source position is within a parameter list, add a selection range covering that whole list. - val selectedParams = - paramss - .iterator - .flatMap: // parameter list to a sourcePosition covering the whole list - case Seq(param) => Some(param.sourcePos) - case params @ Seq(head, tail*) => - val srcPos = head.sourcePos - val lastSpan = tail.last.span - Some(SourcePosition(srcPos.source, srcPos.span union lastSpan, srcPos.outer)) - case Seq() => None - .find(_.contains(pos)) - .map(toSelectionRange) - selectedParams ++ Seq(treeSelectionRange) - case _ => Seq(treeSelectionRange) + def maybeToSelectionRange(srcPos: SourcePosition): Option[SelectionRange] = + if srcPos.contains(pos) then Some(toSelectionRange(srcPos)) else None + + val treeSelectionRange = Seq(toSelectionRange(tree.sourcePos)) + + def allArgsSelectionRange(args: List[Tree]): Option[SelectionRange] = + args match + case Nil => None + case list => + val srcPos = list.head.sourcePos + val lastSpan = list.last.span + val allArgsSrcPos = SourcePosition(srcPos.source, srcPos.span union lastSpan, srcPos.outer) + maybeToSelectionRange(allArgsSrcPos) + + val allSelectionRanges: Iterable[SelectionRange] = tree match + case vdef @ ValDef(_, _, _) => + maybeToSelectionRange(vdef.namePos) + case tdef @ TypeDef(_, _) => + maybeToSelectionRange(tdef.namePos) + case mdef @ ModuleDef(_, _) => + maybeToSelectionRange(mdef.namePos) + case DefDef(_, paramss, _, _) => + paramss.flatMap(allArgsSelectionRange) + case Apply(_, args) => + allArgsSelectionRange(args) + case TypeApply(_, args) => + allArgsSelectionRange(args) + case Function(args, _) => + allArgsSelectionRange(args) + case _ => Seq.empty + allSelectionRanges ++ treeSelectionRange private def setParent( child: SelectionRange, diff --git a/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala index bd16d2ce2aa9..423ca5d8db89 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala @@ -13,7 +13,7 @@ import dotty.tools.pc.utils.InteractiveEnrichments.* import org.eclipse.lsp4j as l import scala.jdk.CollectionConverters.* -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.pc.OffsetParams import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch @@ -37,7 +37,7 @@ object SignatureHelpProvider: val path = Interactive.pathTo(unit.tpdTree, pos.span)(using driver.currentCtx) val localizedContext = Interactive.contextOfPath(path)(using driver.currentCtx) - val indexedContext = IndexedContext(driver.currentCtx) + val indexedContext = IndexedContext(pos)(using driver.currentCtx) given Context = localizedContext.fresh .setCompilationUnit(unit) diff --git a/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala index ccda618078b8..09805fc76040 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/SymbolInformationProvider.scala @@ -11,7 +11,7 @@ import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.StdNames.nme import dotty.tools.dotc.core.Symbols.* -import dotty.tools.pc.utils.InteractiveEnrichments.deepDealias +import dotty.tools.pc.utils.InteractiveEnrichments.deepDealiasAndSimplify import dotty.tools.pc.SemanticdbSymbols import dotty.tools.pc.utils.InteractiveEnrichments.allSymbols import dotty.tools.pc.utils.InteractiveEnrichments.stripBackticks @@ -51,11 +51,17 @@ class SymbolInformationProvider(using Context): collect(classSym) visited.toList.map(SemanticdbSymbols.symbolName) val dealisedSymbol = - if sym.isAliasType then sym.info.deepDealias.typeSymbol else sym + if sym.isAliasType then sym.info.deepDealiasAndSimplify.typeSymbol else sym val classOwner = sym.ownersIterator.drop(1).find(s => s.isClass || s.is(Flags.Module)) val overridden = sym.denot.allOverriddenSymbols.toList - val memberDefAnnots = sym.info.membersBasedOnFlags(Flags.Method, Flags.EmptyFlags).flatMap(_.allSymbols).flatMap(_.denot.annotations) + val memberDefAnnots = + if classSym.exists then + classSym.info + .membersBasedOnFlags(Flags.Method, Flags.EmptyFlags) + .flatMap(_.allSymbols) + .flatMap(_.denot.annotations) + else Nil val pcSymbolInformation = PcSymbolInformation( diff --git a/presentation-compiler/src/main/dotty/tools/pc/WithCompilationUnit.scala b/presentation-compiler/src/main/dotty/tools/pc/WithCompilationUnit.scala index 8110db269b3b..56be6614bbd4 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/WithCompilationUnit.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/WithCompilationUnit.scala @@ -76,7 +76,9 @@ class WithCompilationUnit( } else Set.empty val all = - if sym.is(Flags.ModuleClass) then + if sym.is(Flags.Exported) then + Set(sym, sym.sourceSymbol) + else if sym.is(Flags.ModuleClass) then Set(sym, sym.companionModule, sym.companionModule.companion) else if sym.isClass then Set(sym, sym.companionModule, sym.companion.moduleClass) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteFileCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteFileCompletions.scala index 81337c7d8dcb..58552545355d 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteFileCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteFileCompletions.scala @@ -63,7 +63,7 @@ object AmmoniteFileCompletions: ) def matches(file: Path): Boolean = - (Files.isDirectory(file) || file.toAbsolutePath().toString.isAmmoniteScript) && + (Files.isDirectory(file) || file.toAbsolutePath().toString.isScalaScript) && query.exists(q => CompletionFuzzy.matches(q.nn, file.getFileName().toString)) (split, workspace) match diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionAffix.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionAffix.scala index 4ed58c773a7c..78f9f5f68bfb 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionAffix.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionAffix.scala @@ -56,6 +56,7 @@ case class CompletionAffix( private def loopPrefix(prefixes: List[PrefixKind]): String = prefixes match case PrefixKind.New :: tail => "new " + loopPrefix(tail) + case PrefixKind.Using :: tail => "using " + loopPrefix(tail) case _ => "" /** @@ -87,7 +88,7 @@ enum SuffixKind: case Brace, Bracket, Template, NoSuffix enum PrefixKind: - case New + case New, Using type Suffix = Affix[SuffixKind] type Prefix = Affix[PrefixKind] diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala index 6d89cb663b9c..40f1ccd2e797 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala @@ -23,7 +23,9 @@ case class CompletionPos( query: String, originalCursorPosition: SourcePosition, sourceUri: URI, - withCURSOR: Boolean + withCURSOR: Boolean, + hasLeadingBacktick: Boolean, + hasTrailingBacktick: Boolean ): def queryEnd: Int = originalCursorPosition.point def stripSuffixEditRange: l.Range = new l.Range(originalCursorPosition.offsetToPos(queryStart), originalCursorPosition.offsetToPos(identEnd)) @@ -38,16 +40,35 @@ object CompletionPos: adjustedPath: List[Tree], wasCursorApplied: Boolean )(using Context): CompletionPos = - val identEnd = adjustedPath match + def hasBacktickAt(offset: Int): Boolean = + sourcePos.source.content().lift(offset).contains('`') + + val (identEnd, hasTrailingBacktick) = adjustedPath match case (refTree: RefTree) :: _ if refTree.name.toString.contains(Cursor.value) => - refTree.span.end - Cursor.value.length - case (refTree: RefTree) :: _ => refTree.span.end - case _ => sourcePos.end + val refTreeEnd = refTree.span.end + val hasTrailingBacktick = hasBacktickAt(refTreeEnd - 1) + val identEnd = refTreeEnd - Cursor.value.length + (if hasTrailingBacktick then identEnd - 1 else identEnd, hasTrailingBacktick) + case (refTree: RefTree) :: _ => + val refTreeEnd = refTree.span.end + val hasTrailingBacktick = hasBacktickAt(refTreeEnd - 1) + (if hasTrailingBacktick then refTreeEnd - 1 else refTreeEnd, hasTrailingBacktick) + case _ => (sourcePos.end, false) val query = Completion.completionPrefix(adjustedPath, sourcePos) val start = sourcePos.end - query.length() + val hasLeadingBacktick = hasBacktickAt(start - 1) - CompletionPos(start, identEnd, query.nn, sourcePos, offsetParams.uri.nn, wasCursorApplied) + CompletionPos( + start, + identEnd, + query.nn, + sourcePos, + offsetParams.uri.nn, + wasCursorApplied, + hasLeadingBacktick, + hasTrailingBacktick + ) /** * Infer the indentation by counting the number of spaces in the given line. diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala index adaeadb12978..3d315d5cc26e 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala @@ -4,7 +4,7 @@ package completions import java.nio.file.Path import scala.jdk.CollectionConverters._ -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.SymbolSearch @@ -99,15 +99,15 @@ class CompletionProvider( * 4| $1$.sliding@@[Int](size, step) * */ - if qual.symbol.is(Flags.Synthetic) && qual.symbol.name.isInstanceOf[DerivedName] => + if qual.symbol.is(Flags.Synthetic) && qual.span.isZeroExtent && qual.symbol.name.isInstanceOf[DerivedName] => qual.symbol.defTree match - case valdef: ValDef => Select(valdef.rhs, name) :: tail + case valdef: ValDef if !valdef.rhs.isEmpty => Select(valdef.rhs, name) :: tail case _ => tpdPath0 case _ => tpdPath0 val locatedCtx = Interactive.contextOfPath(tpdPath)(using newctx) - val indexedCtx = IndexedContext(locatedCtx) + val indexedCtx = IndexedContext(pos)(using locatedCtx) val completionPos = CompletionPos.infer(pos, params, adjustedPath, wasCursorApplied)(using locatedCtx) @@ -176,9 +176,10 @@ class CompletionProvider( val text = params.text().nn val offset = params.offset().nn val query = Completion.naiveCompletionPrefix(text, offset) - - if offset > 0 && text.charAt(offset - 1).isUnicodeIdentifierPart - && !CompletionProvider.allKeywords.contains(query) then false -> text + def isValidLastChar = + val lastChar = text.charAt(offset - 1) + lastChar.isUnicodeIdentifierPart || lastChar == '.' + if offset > 0 && isValidLastChar && !CompletionProvider.allKeywords.contains(query) then false -> text else val isStartMultilineComment = @@ -218,9 +219,10 @@ class CompletionProvider( // related issue https://github.com/lampepfl/scala3/issues/11941 lazy val kind: CompletionItemKind = underlyingCompletion.completionItemKind val description = underlyingCompletion.description(printer) - val label = underlyingCompletion.labelWithDescription(printer) + val label = + if config.isDetailIncludedInLabel then completion.labelWithDescription(printer) + else completion.label val ident = underlyingCompletion.insertText.getOrElse(underlyingCompletion.label) - lazy val isInStringInterpolation = path match // s"My name is $name" @@ -246,10 +248,17 @@ class CompletionProvider( range: Option[LspRange] = None ): CompletionItem = val oldText = params.text().nn.substring(completionPos.queryStart, completionPos.identEnd) - val editRange = if newText.startsWith(oldText) then completionPos.stripSuffixEditRange + val trimmedNewText = { + var nt = newText + if (completionPos.hasLeadingBacktick) nt = nt.stripPrefix("`") + if (completionPos.hasTrailingBacktick) nt = nt.stripSuffix("`") + nt + } + + val editRange = if trimmedNewText.startsWith(oldText) then completionPos.stripSuffixEditRange else completionPos.toEditRange - val textEdit = new TextEdit(range.getOrElse(editRange), wrapInBracketsIfRequired(newText)) + val textEdit = new TextEdit(range.getOrElse(editRange), wrapInBracketsIfRequired(trimmedNewText)) val item = new CompletionItem(label) item.setSortText(f"${idx}%05d") diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala index 90b285bffb3a..05d97972d76e 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala @@ -261,13 +261,13 @@ object CompletionValue: end NamedArg case class Autofill( - value: String + value: String, + override val label: String, ) extends CompletionValue: override def completionItemKind(using Context): CompletionItemKind = CompletionItemKind.Enum override def completionItemDataKind: Integer = CompletionSource.OverrideKind.ordinal override def insertText: Option[String] = Some(value) - override def label: String = "Autofill with default values" case class Keyword(label: String, override val insertText: Option[String]) extends CompletionValue: diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 1114a07f91b5..959efb963c27 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -5,7 +5,7 @@ import java.nio.file.Path import java.nio.file.Paths import scala.collection.mutable -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.mtags.CoursierComplete import scala.meta.internal.pc.{IdentifierComparator, MemberOrdering, CompletionFuzzy} import scala.meta.pc.* @@ -74,7 +74,15 @@ class Completions( case tpe :: (appl: AppliedTypeTree) :: _ if appl.tpt == tpe => false case sel :: (funSel @ Select(fun, name)) :: (appl: GenericApply) :: _ if appl.fun == funSel && sel == fun => false - case _ => true) + case _ => true) && + (adjustedPath match + /* In case of `class X derives TC@@` we shouldn't add `[]` + */ + case Ident(_) :: (templ: untpd.DerivingTemplate) :: _ => + val pos = completionPos.toSourcePosition + !templ.derived.exists(_.sourcePos.contains(pos)) + case _ => true + ) private lazy val isNew: Boolean = Completion.isInNewContext(adjustedPath) @@ -193,13 +201,24 @@ class Completions( ) end isAbstractType - private def findSuffix(symbol: Symbol): CompletionAffix = + private def findSuffix(symbol: Symbol, adjustedPath: List[untpd.Tree]): CompletionAffix = CompletionAffix.empty .chain { suffix => // for [] suffix if shouldAddSuffix && symbol.info.typeParams.nonEmpty then suffix.withNewSuffixSnippet(Affix(SuffixKind.Bracket)) else suffix } + .chain{ suffix => + adjustedPath match + case (ident: Ident) :: (app@Apply(_, List(arg))) :: _ => + app.symbol.info match + case mt@MethodType(termNames) if app.symbol.paramSymss.last.exists(_.is(Given)) && + !text.substring(app.fun.span.start, arg.span.end).nn.contains("using") => + suffix.withNewPrefix(Affix(PrefixKind.Using)) + case _ => suffix + case _ => suffix + + } .chain { suffix => // for () suffix if shouldAddSuffix && symbol.is(Flags.Method) then val paramss = getParams(symbol) @@ -273,7 +292,7 @@ class Completions( val existsApply = extraMethodDenots.exists(_.symbol.name == nme.apply) extraMethodDenots.map { methodDenot => - val suffix = findSuffix(methodDenot.symbol) + val suffix = findSuffix(methodDenot.symbol, adjustedPath) val affix = if methodDenot.symbol.isConstructor && existsApply then adjustedPath match case (select @ Select(qual, _)) :: _ => @@ -295,7 +314,7 @@ class Completions( if skipOriginalDenot then extraCompletionValues else - val suffix = findSuffix(denot.symbol) + val suffix = findSuffix(denot.symbol, adjustedPath) val name = undoBacktick(label) val denotCompletionValue = toCompletionValue(name, denot, suffix) denotCompletionValue :: extraCompletionValues @@ -522,7 +541,7 @@ class Completions( config.isCompletionSnippetsEnabled() ) (args, false) - val singletonCompletions = InterCompletionType.inferType(path).map( + val singletonCompletions = InferCompletionType.inferType(path).map( SingletonCompletions.contribute(path, _, completionPos) ).getOrElse(Nil) (singletonCompletions ++ advanced, exclusive) @@ -571,6 +590,7 @@ class Completions( then indexedContext.lookupSym(sym) match case IndexedContext.Result.InScope => false + case IndexedContext.Result.Missing if indexedContext.rename(sym).isDefined => false case _ if completionMode.is(Mode.ImportOrExport) => visit( CompletionValue.Workspace( @@ -589,7 +609,7 @@ class Completions( else false, ) Some(search.search(query, buildTargetIdentifier, visitor).nn) - else if completionMode.is(Mode.Member) then + else if completionMode.is(Mode.Member) && query.nonEmpty then val visitor = new CompilerSearchVisitor(sym => def isExtensionMethod = sym.is(ExtensionMethod) && qualType.widenDealias <:< sym.extensionParam.info.widenDealias diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala index da46e5167834..9cceff7310c6 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala @@ -1,7 +1,7 @@ package dotty.tools.pc.completions import scala.collection.mutable.ListBuffer -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.pc.CompletionFuzzy import scala.meta.internal.pc.InterpolationSplice import scala.meta.pc.PresentationCompilerConfig diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala index 5e809f5b0110..a6122e9ba857 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala @@ -6,7 +6,7 @@ import java.net.URI import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters._ -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.internal.pc.CompletionFuzzy import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.SymbolSearch @@ -93,7 +93,7 @@ object CaseKeywordCompletion: case (Ident(v), tpe) => v.decoded == value case (Select(_, v), tpe) => v.decoded == value case t => false - .map((_, id) => argPts(id).widen.deepDealias) + .map((_, id) => argPts(id).widen.deepDealiasAndSimplify) /* Parent is a function expecting a case match expression */ case TreeApply(fun, _) if !fun.tpe.isErroneous => fun.tpe.paramInfoss match @@ -103,12 +103,12 @@ object CaseKeywordCompletion: ) => val args = head.argTypes.init if args.length > 1 then - Some(definitions.tupleType(args).widen.deepDealias) - else args.headOption.map(_.widen.deepDealias) + Some(definitions.tupleType(args).widen.deepDealiasAndSimplify) + else args.headOption.map(_.widen.deepDealiasAndSimplify) case _ => None case _ => None case sel => - Some(sel.tpe.widen.deepDealias) + Some(sel.tpe.widen.deepDealiasAndSimplify) selTpe .collect { case selTpe if selTpe != NoType => @@ -147,7 +147,7 @@ object CaseKeywordCompletion: definitions.NullClass, definitions.NothingClass, ) - val tpes = Set(selectorSym, selectorSym.companion) + val tpes = Set(selectorSym, selectorSym.companion).filter(_ != NoSymbol) def isSubclass(sym: Symbol) = tpes.exists(par => sym.isSubClass(par)) def visit(symImport: SymbolImport): Unit = @@ -174,8 +174,9 @@ object CaseKeywordCompletion: indexedContext.scopeSymbols .foreach(s => - val ts = s.info.deepDealias.typeSymbol - if isValid(ts) then visit(autoImportsGen.inferSymbolImport(ts)) + val ts = if s.is(Flags.Module) then s.info.typeSymbol else s.dealiasType + if isValid(ts) then + visit(autoImportsGen.inferSymbolImport(ts)) ) // Step 2: walk through known subclasses of sealed types. val sealedDescs = subclassesForType( @@ -185,6 +186,7 @@ object CaseKeywordCompletion: val symbolImport = autoImportsGen.inferSymbolImport(sym) visit(symbolImport) } + val res = result.result().flatMap { case si @ SymbolImport(sym, name, importSel) => completionGenerator.labelForCaseMember(sym, name.value).map { @@ -277,8 +279,8 @@ object CaseKeywordCompletion: clientSupportsSnippets ) - val tpeStr = printer.tpe(selector.tpe.widen.deepDealias.bounds.hi) - val tpe = selector.typeOpt.widen.deepDealias.bounds.hi match + val tpeStr = printer.tpe(selector.tpe.widen.deepDealiasAndSimplify.bounds.hi) + val tpe = selector.typeOpt.widen.deepDealiasAndSimplify.bounds.hi match case tr @ TypeRef(_, _) => tr.underlying case t => t @@ -293,7 +295,6 @@ object CaseKeywordCompletion: val (labels, imports) = sortedSubclasses.map((si, label) => (label, si.importSel)).unzip - val (obracket, cbracket) = if noIndent then (" {", "}") else ("", "") val basicMatch = CompletionValue.MatchCompletion( "match", diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala index dd3a910beb4f..faf6d715d8cf 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/NamedArgCompletions.scala @@ -2,34 +2,23 @@ package dotty.tools.pc.completions import scala.util.Try -import dotty.tools.dotc.ast.Trees.ValDef import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.ast.untpd import dotty.tools.dotc.core.Constants.Constant -import dotty.tools.dotc.core.ContextOps.localContext import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags -import dotty.tools.dotc.core.Flags.Method import dotty.tools.dotc.core.NameKinds.DefaultGetterName import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.StdNames.* -import dotty.tools.dotc.core.SymDenotations.NoDenotation -import dotty.tools.dotc.core.Symbols import dotty.tools.dotc.core.Symbols.defn -import dotty.tools.dotc.core.Symbols.NoSymbol import dotty.tools.dotc.core.Symbols.Symbol -import dotty.tools.dotc.core.Types.AndType -import dotty.tools.dotc.core.Types.AppliedType -import dotty.tools.dotc.core.Types.MethodType -import dotty.tools.dotc.core.Types.OrType -import dotty.tools.dotc.core.Types.RefinedType -import dotty.tools.dotc.core.Types.TermRef -import dotty.tools.dotc.core.Types.Type -import dotty.tools.dotc.core.Types.TypeBounds -import dotty.tools.dotc.core.Types.WildcardType +import dotty.tools.dotc.core.Types.* import dotty.tools.pc.IndexedContext import dotty.tools.pc.utils.InteractiveEnrichments.* import scala.annotation.tailrec +import dotty.tools.pc.ApplyArgsExtractor +import dotty.tools.pc.ParamSymbol +import dotty.tools.pc.ApplyExtractor object NamedArgCompletions: @@ -40,36 +29,13 @@ object NamedArgCompletions: clientSupportsSnippets: Boolean, )(using ctx: Context): List[CompletionValue] = path match - case (ident: Ident) :: ValDef(_, _, _) :: Block(_, app: Apply) :: _ - if !app.fun.isInfix => + case (ident: Ident) :: ApplyExtractor(app) => contribute( - Some(ident), + ident, app, indexedContext, clientSupportsSnippets, ) - case (ident: Ident) :: rest => - def getApplyForContextFunctionParam(path: List[Tree]): Option[Apply] = - path match - // fun(arg@@) - case (app: Apply) :: _ => Some(app) - // fun(arg@@), where fun(argn: Context ?=> SomeType) - // recursively matched for multiple context arguments, e.g. Context1 ?=> Context2 ?=> SomeType - case (_: DefDef) :: Block(List(_), _: Closure) :: rest => - getApplyForContextFunctionParam(rest) - case _ => None - val contribution = - for - app <- getApplyForContextFunctionParam(rest) - if !app.fun.isInfix - yield - contribute( - Some(ident), - app, - indexedContext, - clientSupportsSnippets, - ) - contribution.getOrElse(Nil) case (app: Apply) :: _ => /** * def foo(aaa: Int, bbb: Int, ccc: Int) = ??? @@ -83,7 +49,7 @@ object NamedArgCompletions: untypedPath match case (ident: Ident) :: (app: Apply) :: _ => contribute( - Some(ident), + ident, app, indexedContext, clientSupportsSnippets, @@ -96,7 +62,7 @@ object NamedArgCompletions: end contribute private def contribute( - ident: Option[Ident], + ident: Ident, apply: Apply, indexedContext: IndexedContext, clientSupportsSnippets: Boolean, @@ -107,159 +73,14 @@ object NamedArgCompletions: case Literal(Constant(null)) => true // nullLiteral case _ => false - def collectArgss(a: Apply): List[List[Tree]] = - def stripContextFuntionArgument(argument: Tree): List[Tree] = - argument match - case Block(List(d: DefDef), _: Closure) => - d.rhs match - case app: Apply => - app.args - case b @ Block(List(_: DefDef), _: Closure) => - stripContextFuntionArgument(b) - case _ => Nil - case v => List(v) - - val args = a.args.flatMap(stripContextFuntionArgument) - a.fun match - case app: Apply => collectArgss(app) :+ args - case _ => List(args) - end collectArgss - - val method = apply.fun - - val argss = collectArgss(apply) - - def fallbackFindApply(sym: Symbol) = - sym.info.member(nme.apply) match - case NoDenotation => Nil - case den => List(den.symbol) - - // fallback for when multiple overloaded methods match the supplied args - def fallbackFindMatchingMethods() = - def maybeNameAndIndexedContext( - method: Tree - ): Option[(Name, IndexedContext)] = - method match - case Ident(name) => Some((name, indexedContext)) - case Select(This(_), name) => Some((name, indexedContext)) - case Select(from, name) => - val symbol = from.symbol - val ownerSymbol = - if symbol.is(Method) && symbol.owner.isClass then - Some(symbol.owner) - else Try(symbol.info.classSymbol).toOption - ownerSymbol.map(sym => - (name, IndexedContext(context.localContext(from, sym))) - ) - case Apply(fun, _) => maybeNameAndIndexedContext(fun) - case _ => None - val matchingMethods = - for - (name, indexedContext) <- maybeNameAndIndexedContext(method) - potentialMatches <- indexedContext.findSymbol(name) - yield - potentialMatches.collect { - case m - if m.is(Flags.Method) && - m.vparamss.length >= argss.length && - Try(m.isAccessibleFrom(apply.symbol.info)).toOption - .getOrElse(false) && - m.vparamss - .zip(argss) - .reverse - .zipWithIndex - .forall { case (pair, index) => - FuzzyArgMatcher(m.tparams) - .doMatch(allArgsProvided = index != 0, ident) - .tupled(pair) - } => - m - } - matchingMethods.getOrElse(Nil) - end fallbackFindMatchingMethods - - val matchingMethods: List[Symbols.Symbol] = - if method.symbol.paramSymss.nonEmpty then - val allArgsAreSupplied = - val vparamss = method.symbol.vparamss - vparamss.length == argss.length && vparamss - .zip(argss) - .lastOption - .exists { case (baseParams, baseArgs) => - baseArgs.length == baseParams.length - } - // ``` - // m(arg : Int) - // m(arg : Int, anotherArg : Int) - // m(a@@) - // ``` - // complier will choose the first `m`, so we need to manually look for the other one - if allArgsAreSupplied then - val foundPotential = fallbackFindMatchingMethods() - if foundPotential.contains(method.symbol) then foundPotential - else method.symbol :: foundPotential - else List(method.symbol) - else if method.symbol.is(Method) || method.symbol == NoSymbol then - fallbackFindMatchingMethods() - else fallbackFindApply(method.symbol) - end if - end matchingMethods - - val allParams = matchingMethods.flatMap { methodSym => - val vparamss = methodSym.vparamss - - // get params and args we are interested in - // e.g. - // in the following case, the interesting args and params are - // - params: [apple, banana] - // - args: [apple, b] - // ``` - // def curry(x: Int)(apple: String, banana: String) = ??? - // curry(1)(apple = "test", b@@) - // ``` - val (baseParams0, baseArgs) = - vparamss.zip(argss).lastOption.getOrElse((Nil, Nil)) + val argsAndParams = ApplyArgsExtractor.getArgsAndParams( + Some(indexedContext), + apply, + ident.span + ) - val baseParams: List[ParamSymbol] = - def defaultBaseParams = baseParams0.map(JustSymbol(_)) - @tailrec - def getRefinedParams(refinedType: Type, level: Int): List[ParamSymbol] = - if level > 0 then - val resultTypeOpt = - refinedType match - case RefinedType(AppliedType(_, args), _, _) => args.lastOption - case AppliedType(_, args) => args.lastOption - case _ => None - resultTypeOpt match - case Some(resultType) => getRefinedParams(resultType, level - 1) - case _ => defaultBaseParams - else - refinedType match - case RefinedType(AppliedType(_, args), _, MethodType(ri)) => - baseParams0.zip(ri).zip(args).map { case ((sym, name), arg) => - RefinedSymbol(sym, name, arg) - } - case _ => defaultBaseParams - // finds param refinements for lambda expressions - // val hello: (x: Int, y: Int) => Unit = (x, _) => println(x) - @tailrec - def refineParams(method: Tree, level: Int): List[ParamSymbol] = - method match - case Select(Apply(f, _), _) => refineParams(f, level + 1) - case Select(h, name) => - // for Select(foo, name = apply) we want `foo.symbol` - if name == nme.apply then getRefinedParams(h.symbol.info, level) - else getRefinedParams(method.symbol.info, level) - case Apply(f, _) => - refineParams(f, level + 1) - case _ => getRefinedParams(method.symbol.info, level) - refineParams(method, 0) - end baseParams - - val args = ident - .map(i => baseArgs.filterNot(_ == i)) - .getOrElse(baseArgs) - .filterNot(isUselessLiteral) + val allParams = argsAndParams.flatMap { case (baseArgs, baseParams) => + val args = baseArgs.filterNot( a => a == ident || isUselessLiteral(a)) @tailrec def isDefaultArg(t: Tree): Boolean = t match @@ -294,9 +115,8 @@ object NamedArgCompletions: ) } - val prefix = ident - .map(_.name.toString) - .getOrElse("") + val prefix = + ident.name.toString .replace(Cursor.value, "") .nn @@ -331,7 +151,7 @@ object NamedArgCompletions: allParams.exists(param => param.name.startsWith(prefix)) def isExplicitlyCalled = suffix.startsWith(prefix) def hasParamsToFill = allParams.count(!_.symbol.is(Flags.HasDefault)) > 1 - if clientSupportsSnippets && matchingMethods.length == 1 && (shouldShow || isExplicitlyCalled) && hasParamsToFill + if clientSupportsSnippets && argsAndParams.length == 1 && (shouldShow || isExplicitlyCalled) && hasParamsToFill then val editText = allParams.zipWithIndex .collect { @@ -339,9 +159,16 @@ object NamedArgCompletions: s"${param.nameBackticked.replace("$", "$$")} = $${${index + 1}${findDefaultValue(param)}}" } .mkString(", ") + val labelText = allParams + .collect { + case param if !param.symbol.is(Flags.HasDefault) => + s"${param.nameBackticked.replace("$", "$$")} = ???" + } + .mkString(", ") List( CompletionValue.Autofill( - editText + editText, + labelText, ) ) else List.empty @@ -369,73 +196,4 @@ object NamedArgCompletions: ) ::: findPossibleDefaults() ::: fillAllFields() end contribute - extension (method: Symbols.Symbol) - def vparamss(using Context) = method.filteredParamss(_.isTerm) - def tparams(using Context) = method.filteredParamss(_.isType).flatten - def filteredParamss(f: Symbols.Symbol => Boolean)(using Context) = - method.paramSymss.filter(params => params.forall(f)) end NamedArgCompletions - -class FuzzyArgMatcher(tparams: List[Symbols.Symbol])(using Context): - - /** - * A heuristic for checking if the passed arguments match the method's arguments' types. - * For non-polymorphic methods we use the subtype relation (`<:<`) - * and for polymorphic methods we use a heuristic. - * We check the args types not the result type. - */ - def doMatch( - allArgsProvided: Boolean, - ident: Option[Ident] - )(expectedArgs: List[Symbols.Symbol], actualArgs: List[Tree]) = - (expectedArgs.length == actualArgs.length || - (!allArgsProvided && expectedArgs.length >= actualArgs.length)) && - actualArgs.zipWithIndex.forall { - case (arg: Ident, _) if ident.contains(arg) => true - case (NamedArg(name, arg), _) => - expectedArgs.exists { expected => - expected.name == name && (!arg.hasType || arg.typeOpt.unfold - .fuzzyArg_<:<(expected.info)) - } - case (arg, i) => - !arg.hasType || arg.typeOpt.unfold.fuzzyArg_<:<(expectedArgs(i).info) - } - - extension (arg: Type) - def fuzzyArg_<:<(expected: Type) = - if tparams.isEmpty then arg <:< expected - else arg <:< substituteTypeParams(expected) - def unfold = - arg match - case arg: TermRef => arg.underlying - case e => e - - private def substituteTypeParams(t: Type): Type = - t match - case e if tparams.exists(_ == e.typeSymbol) => - val matchingParam = tparams.find(_ == e.typeSymbol).get - matchingParam.info match - case b @ TypeBounds(_, _) => WildcardType(b) - case _ => WildcardType - case o @ OrType(e1, e2) => - OrType(substituteTypeParams(e1), substituteTypeParams(e2), o.isSoft) - case AndType(e1, e2) => - AndType(substituteTypeParams(e1), substituteTypeParams(e2)) - case AppliedType(et, eparams) => - AppliedType(et, eparams.map(substituteTypeParams)) - case _ => t - -end FuzzyArgMatcher - -sealed trait ParamSymbol: - def name: Name - def info: Type - def symbol: Symbol - def nameBackticked(using Context) = name.decoded.backticked - -case class JustSymbol(symbol: Symbol)(using Context) extends ParamSymbol: - def name: Name = symbol.name - def info: Type = symbol.info - -case class RefinedSymbol(symbol: Symbol, name: Name, info: Type) - extends ParamSymbol diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala index f5c15ca6df0e..f01a1e9b8cd8 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/OverrideCompletions.scala @@ -4,7 +4,7 @@ package completions import java.util as ju import scala.jdk.CollectionConverters._ -import scala.meta.internal.metals.ReportContext +import scala.meta.pc.reports.ReportContext import scala.meta.pc.OffsetParams import scala.meta.pc.PresentationCompilerConfig import scala.meta.pc.PresentationCompilerConfig.OverrideDefFormat @@ -191,7 +191,7 @@ object OverrideCompletions: template :: path case path => path - val indexedContext = IndexedContext( + val indexedContext = IndexedContext(pos)(using Interactive.contextOfPath(path)(using newctx) ) import indexedContext.ctx @@ -511,10 +511,10 @@ object OverrideCompletions: Context ): Option[Int] = defn match - case td: TypeDef if text.charAt(td.rhs.span.end) == ':' => + case td: TypeDef if (td.rhs.span.end < text.length) && text.charAt(td.rhs.span.end) == ':' => Some(td.rhs.span.end) case TypeDef(_, temp : Template) => - temp.parentsOrDerived.lastOption.map(_.span.end).filter(text.charAt(_) == ':') + temp.parentsOrDerived.lastOption.map(_.span.end).filter(idx => text.length > idx && text.charAt(idx) == ':') case _ => None private def fallbackFromParent(parent: Tree, name: String)(using Context) = diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala index e2a0a033ee6b..8df727b14155 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/ScalaCliCompletions.scala @@ -13,14 +13,20 @@ class ScalaCliCompletions( ): def unapply(path: List[Tree]) = def scalaCliDep = CoursierComplete.isScalaCliDep( - pos.lineContent.take(pos.column).stripPrefix("/*