matrei commented on PR #16322:
URL: https://github.com/apache/grails-core/pull/16322#issuecomment-5615272342

   ## AI Review Findings
   
   Head `8855b88340`, base `8.0.x` at `0980623481`. The head is 103 commits 
behind the base but merges clean (`git merge-tree` reports no conflicts). Both 
defects in the description are real and both fixes work:
   
   - Compiling the base-branch welcome page statically (copied into 
`grails-test-examples/gsp-compile-static`) fails with 4 type-checking errors at 
three sites, the plugins sort, the domains-by-plugin sort and the mime-types 
sort. The PR's page compiles, and the compiled class extends 
`CompileStaticGroovyPage`, so it really was compiled statically rather than 
falling back.
   - The scaffolded `index.gsp` expanded for a domain class compiles statically 
with `Number` and the `${count} > params.int('max')` comparison type-checks.
   - `GspCompileStaticSpec`: 30 tests pass. Checkstyle on `grails-gsp-core` 
main: 0 violations.
   
   The findings below are about the choice the `GroovyPage` change makes, and 
about what would keep either bug from coming back. None of them changes the 
welcome page hunks.
   
   ### [P2] Assign model fields the way Groovy assigns, instead of declaring 
that model values are not coerced
   
   References:
   
   - `grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java:180-197` 
(`applyModelFieldsFromBinding`)
   - `grails-scaffolding/src/main/templates/scaffolding/index.gsp:1`
   
   The new message states a rule, "model values are not coerced", that is the 
opposite of what the rest of the page does. `<g:set type="int" var="total" 
value="${books.size()}"/>` converts (the static compilation guide says so 
explicitly), and plain Groovy converts too:
   
   ```groovy
   Integer i = 42L      // 42, java.lang.Integer
   int p = 42L          // 42
   String s = "${40+2}" // GString assigned to String
   ```
   
   A model field declared `Integer bookCount` is the one place in a page where 
the same assignment throws. That declaration is also the natural one to write: 
GORM's `count()` returns `Integer` (`GormEntity.groovy:732`), the generated 
service returns `Long`, and `RestfulController.countResources()` returns 
`Integer`, so any page author who declares the count with the type of whichever 
source they looked at has a coin-flip chance of a render failure. `String` 
fields fed a GString from a controller fail the same way today.
   
   `DefaultTypeTransformation.castToType` is Groovy's own assignment 
conversion, so it gives exactly the semantics a `def`-free Groovy assignment 
would:
   
   ```java
   try {
       field.set(this, DefaultTypeTransformation.castToType(value, 
field.getType()));
   } catch (IllegalArgumentException | GroovyCastException e) {
       throw new GroovyPagesException("Model field '" + field.getName() + "' is 
declared as " +
               field.getType().getName() + " but the model supplied an instance 
of " +
               value.getClass().getName() + '.', e, -1, 
getGroovyPageFileName());
   }
   ```
   
   I ran the PR's spec with that in place plus a data-driven case:
   
   | declared | supplied | result |
   |---|---|---|
   | `Integer` | `42L` | renders `42` |
   | `int` | `42L` | renders `42` |
   | `Long` | `42` | renders `42` |
   | `long` | `42 as Short` | renders `42` |
   | `String` | `"${40 + 2}"` (GString) | renders `42` |
   | `Number` | `42 as BigInteger` | renders `42` |
   | `Integer` | `new Date()` | `GroovyPagesException` naming the field, cause 
`GroovyCastException` |
   
   The only existing test that changes is *a model value of the wrong type 
names the field and both types*, which would flip from `Integer`/`Long` to a 
pair that genuinely cannot be converted, `Integer`/`Date` say. Everything else 
in the spec passes unchanged. The prototype is not in the working tree.
   
   The `Number` change in the scaffold template is still the honest type for a 
value that is `Long` from one controller and `Integer` from the other, so keep 
it either way. With coercion it stops being load-bearing, and every 
already-generated `Integer` view in existing 8.0.x applications starts working 
as well, which the template change alone does not give them.
   
   ### [P2] Document what a declared model field does with a value of another 
type
   
   References:
   
   - `grails-doc/src/en/guide/theWebLayer/gsp/gspStaticCompilation.adoc:28-56` 
(Declaring the Model)
   - `grails-doc/src/en/guide/theWebLayer/gsp/gspStaticCompilation.adoc:208`
   
   The guide says nothing about what happens when the supplied value's type 
differs from the declaration. The only nearby sentence, at line 208, covers the 
framework-supplied names and says they fail with a `GroovyCastException`. 
Whichever way the finding above is decided, that section needs one sentence: 
either "a model value is converted the way a Groovy assignment converts it, so 
a `Long` count satisfies an `Integer` field" or "a model value must be 
assignable to the declared type; a mismatch fails at render naming the field". 
The behaviour is user-facing and is exactly what this PR changes, so this is 
the doc coverage the contributing rules ask for.
   
   ### [P2] Neither failure had a test that could catch it, and this PR adds 
coverage for the engine but not for the two pages
   
   References:
   
   - `grails-test-examples/gsp-compile-static/build.gradle`
   - 
`grails-test-examples/scaffolding-fields/grails-app/controllers/scaffoldingfields/EmployeeController.groovy:32`
   - 
`grails-gsp/core/src/test/groovy/org/grails/gsp/GspCompileStaticSpec.groovy:241-274`
   
   CI was green on `8.0.x` with both bugs present, and stays green if either 
regresses:
   
   - Nothing compiles the welcome page statically. `gsp-compile-static` has its 
own small pages only. A `Copy` step that drops 
`grails-profiles/web/skeleton/grails-app/views/index.gsp` into that app's views 
ahead of `compileGroovyPages` is the check I ran by hand above, and it fails on 
the base branch with the four errors. The forge copy is already pinned to the 
profile copy by *test the profile skeleton mirrors the forge welcome templates* 
in `GrailsGspSpec`, so one copy is enough.
   - Nothing renders a service-backed scaffolded index. `scaffolding-fields` 
and `hyphenated` use `static scaffold = Domain`, which goes through 
`RestfulServiceController.countResources()` and supplies an `Integer`, so the 
generated-service `Long` path is never rendered. The new spec cases prove the 
engine accepts a `Long` for a `Number` field; they do not prove the template 
declares `Number`. A test that expands `scaffolding/index.gsp` for a domain 
class and renders it with `[bookList: [], bookCount: 3L]` would pin the 
template itself.
   
   ### Nit: the listener sort hunk is not needed
   
   References:
   
   - `grails-profiles/web/skeleton/grails-app/views/index.gsp:570`
   - `grails-forge/grails-forge-core/src/main/resources/gsp/index.gsp:570`
   
   `Map<String, String> a, Map<String, String> b` on the listener sort compiles 
either way: the `collect` above it builds maps whose values are all `String`, 
so `a.name` already infers as `String`. I reverted that one hunk on the PR's 
page and it compiled. The description says four closures fail; three do. 
Harmless, and fine to keep for symmetry with the other three.
   
   ### Nit: the scaffolding guide describes model names that the templates no 
longer use
   
   References:
   
   - `grails-doc/src/en/guide/scaffolding.adoc:191`
   
   Pre-existing and out of scope, noting it because it is the doc a reader 
would go to for the count field: it says the standard views expect 
`<propertyName>InstanceList` and `<propertyName>Instance`, while the templates 
bind `<propertyName>List` and `<propertyName>Count`.
   
   ### Confirmed
   
   - The `grails.plugins.GrailsPlugin` fully-qualified cast is needed: 
`GroovyPageParser.DEFAULT_IMPORTS` imports 
`grails.plugins.metadata.GrailsPlugin` into every page, so a page import of the 
plugin interface would clash.
   - The rewritten sorts keep their ordering. The one-argument `sort` on the 
plugin rows and on the `domainsByPlugin` entries orders by the same lower-cased 
key the comparators used, and `List.sort(Closure)` still sorts in place as 
before.
   - The `groupBy` closure now returns a typed `String`, which is what lets 
`it.key.toLowerCase()` type-check on the entry.
   - Reading the model value before the `try` changes nothing: only 
`IllegalAccessException` was caught before, so a failing `getProperty` 
propagated then and propagates now.
   
   ## Verification
   
   - `./gradlew :grails-gsp-core:test --tests 
org.grails.gsp.GspCompileStaticSpec`: 30 tests, 0 failures on the PR head.
   - `./gradlew :grails-gsp-core:checkstyleMain`: 0 violations.
   - `./gradlew :grails-test-examples-gsp-compile-static:compileGroovyPages` 
with the PR's welcome page and an expanded scaffold `index.gsp` 
(`List<gspstatic.Book> bookList; Number bookCount`) copied into the app: 
success, both classes extend `CompileStaticGroovyPage`.
   - Same task with the merge-base welcome page: fails, `Cannot find matching 
method java.lang.Object#toLowerCase()` at generated lines 42, 413 and 1340 plus 
`No such property: name for class: java.lang.Object` at 1340.
   - Same task with the PR's page and only the listener sort hunk reverted: 
success.
   - Coercion prototype in `GroovyPage.java` plus 7 temporary spec cases: 37 
tests, the single failure being the PR's `Integer`/`Long` mismatch case, as 
expected. Both files restored afterwards; `git status` shows no tracked changes.
   - The copied pages were removed from 
`grails-test-examples/gsp-compile-static` afterwards.
   


-- 
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