aglinxinyuan opened a new pull request, #7873:
URL: https://github.com/apache/texera/pull/7873

   ### What changes were proposed in this PR?
   
   `EncodableInspector.safeAccessed` hopped from an accessor symbol to 
`accessed` unconditionally. A trait member has no backing field of its own, so 
its accessor's `accessed` is `NoSymbol` — and that is exactly where scalac 
leaves the marker for a trait `val`/`var`, since `@EncodableStringAnnotation` 
targets `METHOD` as well as `FIELD`. The hop threw the annotation away, and the 
failure direction is the dangerous one:
   
   ```
   Before:  trait T { @EncodableStringAnnotation val s: String }
            pyb"print(${t.s})"  ->  PyLiteralStringRenderer  ->  emitted raw, 
no diagnostic
   After:                       ->  EncodableStringRenderer  ->  decode(...) at 
run time
   ```
   
   Nothing warned about it at either end. 
`BoundaryValidator.validateCompileTime` runs only for arguments the inspector 
classified Encodable (`PythonTemplateBuilder.scala:358`), so a trait member 
spliced into an unsafe position skipped the compile-time boundary check as well.
   
   The hop is now conditional — fall back to the accessor when there is no 
field to hop to:
   
   ```scala
   case accessor: TermSymbol if accessor.isAccessor =>
     val field = accessor.accessed
     if (field == null || field == NoSymbol) accessor else field
   case _ => sym
   ```
   
   Measured one shape per row, with a throwaway probe macro that dumped the 
symbol and type state at expansion time (`@Enc` abbreviates 
`@EncodableStringAnnotation`):
   
   | Declaration | `accessed` | Marker sits on | Before | After |
   | --- | --- | --- | --- | --- |
   | `trait T { @Enc val s: String }` | `NoSymbol` | the accessor | raw | 
**encoded** |
   | `trait T { @Enc val s: String = "x" }` | `NoSymbol` | the accessor | raw | 
**encoded** |
   | `trait T { @Enc var s: String = "x" }` | `NoSymbol` | the accessor | raw | 
**encoded** |
   | `class C { @Enc val s: String = "x" }` | the field | the field | encoded | 
unchanged |
   | `case class H(@Enc ui: String)` | the field | the ctor param | raw | 
unchanged |
   | `abstract class C { @Enc val s: String }` | `NoSymbol` | — | raw | raw |
   
   Two rows deliberately do not move. The case-class one is the documented 
meta-annotation rule: without a meta-annotation the marker stays on the 
constructor parameter, and `@(EncodableStringAnnotation @field)` is the shape 
that reaches the field. The abstract-class one is out of reach from here — 
scalac keeps the marker on neither the accessor (its `annotations` is empty 
once its info is forced) nor any field, so the macro never sees it. Type 
position works there, `val s: String @EncodableStringAnnotation`, which is what 
the `EncodableString` alias expands to anyway; the spec asserts that working 
shape rather than cementing the hole with a negative test.
   
   `safeAccessed`'s second case, `case methodAccessor: MethodSymbol if 
methodAccessor.isAccessor`, goes at the same time. `MethodSymbol` is a subtype 
of `TermSymbol` in scala-reflect, so anything that reached it had already 
matched the first case: it could never fire, and it was not a trait path in 
disguise.
   
   ### Any related issues, documentation, discussions?
   
   Stacked on #7864, which added the probe macro these tests use and 
deliberately left this path unpinned so that fixing it would not have to fight 
a test that had cemented it. Until that merges, the diff shown against `main` 
includes its spec commit — only the second commit belongs to this PR.
   
   #7864 also recorded `methodReturnHasAnn` as unpinnable without a production 
seam. That turned out to be wrong for a reason worth writing down; it is pinned 
here, and the stale comment is corrected.
   
   ### How was this PR tested?
   
   Five tests added to `EncodableInspectorSpec`, all driven through #7864's 
probe macro so they read the classifier's answers directly instead of inferring 
them from compile-error text:
   
   | Test | Pins |
   | --- | --- |
   | `a trait's marked val is Encodable even though its accessor has no backing 
field` | the defect itself, over three fixtures: abstract `val`, concrete 
`val`, `var` |
   | `a marked trait val is lowered to an EncodableStringRenderer` | what the 
classification is *for* — `wrapArg` must reach priority 2, not the raw-literal 
default |
   | `an unmarked trait val stays a plain literal` | the fallback reads the 
accessor's own annotations, and reads them *selectively*: a second fixture puts 
a foreign `@(ZzAnnG @getter)` annotation there |
   | `an abstract class's abstract val needs the marker in type position` | the 
working shape for the row the fallback cannot reach |
   | `a def whose inline-annotated result type is stripped at the call site is 
Encodable` | `methodReturnHasAnn` |
   
   Written before the fix. Mutation testing on the touched code — 4 mutants, 4 
killed:
   
   | Mutant | Killed by |
   | --- | --- |
   | revert `safeAccessed` to the unconditional hop (the defect) | the two 
trait-member tests |
   | never hop to the field, `case accessor ... => accessor` | the pre-existing 
`@(EncodableStringAnnotation @field)` test |
   | symbol scan `annotations.exists(annIsEncodableString)` -> 
`annotations.nonEmpty` | `an unmarked trait val stays a plain literal` |
   | `case m: MethodSymbol => typeHasEncodableString(...)` -> `case _: 
MethodSymbol => false` | `an abstract class's abstract val ...` and `a def 
whose inline-annotated result type ...` |
   
   The last row is the gap #7864 recorded. The trait-member fixture is not what 
closes it — a trait `val` is detected through the symbol path, not the 
signature path. A different measurement is: `EncodableString` is a type 
*alias*, so on `def ui: EncodableString` the call-site tree's own type 
dealiases back to the annotated type and the last disjunct answers true on its 
own, which is why the arm looked untestable. An *inline* `def ui: String 
@EncodableStringAnnotation` behaves differently — scalac strips the annotation 
off the call-site tree, leaving `m.typeSignature.finalResultType` the only 
place the marker survives.
   
   ```bash
   sbt "PyBuilder/clean" "PyBuilder/test"
   ```
   
   `Tests: succeeded 189, failed 0` across 5 suites, 184 before.
   
   ```bash
   sbt "PyBuilder/scalafmtCheckAll" "PyBuilder/scalafixAll --check"
   ```
   
   Clean.
   
   ### Was this PR authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code (Opus 5)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to