codeconsole commented on code in PR #16134:
URL: https://github.com/apache/grails-core/pull/16134#discussion_r3793084250
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/gsp/GroovyPagePlugin.groovy:
##########
@@ -64,20 +119,114 @@ class GroovyPagePlugin implements Plugin<Project> {
Provider<Directory> webappDestDir =
project.layout.buildDirectory.dir('gsp-classes/webapp')
output?.dir('gsp-classes')
+ // The Java the rest of the project is built with, so that pages are
built with it too.
+ // Absent a toolchain this resolves to the JVM running Gradle, which
is what compiling
+ // pages fell back to before and remains the right answer when nothing
else was asked for.
+ JavaPluginExtension javaExtension =
project.extensions.getByType(JavaPluginExtension)
+ JavaToolchainService toolchains =
project.extensions.getByType(JavaToolchainService)
+ Provider<JavaLauncher> launcher =
toolchains.launcherFor(javaExtension.toolchain)
+
+ // The index is written twice, because the two things that read it
need different guarantees.
+ //
+ // This one exists before this project is compiled, so that a call to
a tag the project itself
+ // declares can be resolved as it compiles. It is read from source, so
it cannot describe
+ // everything: a tag library referring to a type written in another
language, or generated by
+ // the build, is left out, and what was missed is recorded so that
nothing in an incompletely
+ // described namespace is reported as a misspelling. It is never
packaged - a consumer must not
+ // be given a partial description - and pages are not compiled against
it either.
+ // Everything both indexes must agree on is configured once, by type.
Configuring the two
+ // tasks separately would let them describe different sets of tag
libraries, and the one that
+ // is published is not the one this project compiles against - so they
would diverge silently.
+ // A project keeping tag libraries elsewhere adds them the same way.
+ tasks.withType(GenerateTagLibraryIndexTask).configureEach {
GenerateTagLibraryIndexTask index ->
+
index.sourceDirectories.from(project.layout.projectDirectory.dir('grails-app/taglib'))
+
index.parameterNamesRetained.set(resolvePreserveParameterNames(project))
Review Comment:
Rather than refactor on suspicion I added `--configuration-cache` to
`TagLibraryIndexWiringFunctionalSpec`, storing an entry and then reusing it. It
passes, so the values are resolved at store time as hoped and the code is
unchanged. The test will catch it if that stops being true.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/gsp/GroovyPagePlugin.groovy:
##########
@@ -64,20 +119,114 @@ class GroovyPagePlugin implements Plugin<Project> {
Provider<Directory> webappDestDir =
project.layout.buildDirectory.dir('gsp-classes/webapp')
output?.dir('gsp-classes')
+ // The Java the rest of the project is built with, so that pages are
built with it too.
+ // Absent a toolchain this resolves to the JVM running Gradle, which
is what compiling
+ // pages fell back to before and remains the right answer when nothing
else was asked for.
+ JavaPluginExtension javaExtension =
project.extensions.getByType(JavaPluginExtension)
+ JavaToolchainService toolchains =
project.extensions.getByType(JavaToolchainService)
+ Provider<JavaLauncher> launcher =
toolchains.launcherFor(javaExtension.toolchain)
+
+ // The index is written twice, because the two things that read it
need different guarantees.
+ //
+ // This one exists before this project is compiled, so that a call to
a tag the project itself
+ // declares can be resolved as it compiles. It is read from source, so
it cannot describe
+ // everything: a tag library referring to a type written in another
language, or generated by
+ // the build, is left out, and what was missed is recorded so that
nothing in an incompletely
+ // described namespace is reported as a misspelling. It is never
packaged - a consumer must not
+ // be given a partial description - and pages are not compiled against
it either.
+ // Everything both indexes must agree on is configured once, by type.
Configuring the two
+ // tasks separately would let them describe different sets of tag
libraries, and the one that
+ // is published is not the one this project compiles against - so they
would diverge silently.
+ // A project keeping tag libraries elsewhere adds them the same way.
+ tasks.withType(GenerateTagLibraryIndexTask).configureEach {
GenerateTagLibraryIndexTask index ->
+
index.sourceDirectories.from(project.layout.projectDirectory.dir('grails-app/taglib'))
+
index.parameterNamesRetained.set(resolvePreserveParameterNames(project))
+ index.strictTags.set(resolveStrictTags(project))
+
index.dynamicTagNamespaces.set(resolveDynamicTagNamespaces(project))
+ index.javaLauncher.convention(launcher)
+ }
+
+ // The settings live apart from the descriptors. The descriptors are
published; the settings
+ // say how this project is compiled and must reach no one else, and a
directory on the runtime
+ // classpath is copied wholesale into an executable archive, where
excluding a file from an
+ // archive task cannot reach it.
+ Provider<Directory> settingsDir =
project.layout.buildDirectory.dir('generated/grails-taglib-settings')
+ Provider<Directory> tagLibIndexDir =
project.layout.buildDirectory.dir('generated/grails-taglibs')
+ def generateTagLibraryIndex =
tasks.register('generateTagLibraryIndex', GenerateTagLibraryIndexTask) {
+ it.destinationDirectory.set(tagLibIndexDir)
+ it.settingsDirectory.set(settingsDir)
+
it.generatorClasspath.from(project.configurations.named('compileClasspath'))
+ // A tag library referring to a service, base class or trait of
this project needs that
+ // source to be read, not guessed, or it would be described
wrongly or not at all.
+ it.resolutionSourceRoots.from(project.provider {
resolveGroovySourceRoots(mainSourceSet) })
+ }
+ FileCollection tagLibIndex =
project.files(tagLibIndexDir).builtBy(generateTagLibraryIndex)
+
+ // And this one is written again once the project has been compiled,
with its own classes on
+ // the classpath, where every tag library resolves whatever language
it was written in. It is
+ // the authoritative index: the one pages are compiled against, the
one packaged, and the one a
+ // project depending on this one reads. Every run replaces the
directory, so a renamed or
+ // deleted tag library cannot survive in it.
+ Provider<Directory> packagedIndexDir =
+
project.layout.buildDirectory.dir('generated/grails-taglibs-packaged')
+ Provider<Directory> packagedSettingsDir =
+
project.layout.buildDirectory.dir('generated/grails-taglib-settings-packaged')
+ def packageTagLibraryIndex = tasks.register('packageTagLibraryIndex',
GenerateTagLibraryIndexTask) {
+ it.description = 'Regenerates the tag library index against the
compiled project'
+ it.destinationDirectory.set(packagedIndexDir)
+ it.settingsDirectory.set(packagedSettingsDir)
+ // The compiled classes, and a dependency on the task that gathers
them, so this waits
+ // for everything that writes into those directories rather than
for the compile tasks
+ // alone - the ast classes are copied in after compiling, for one.
+ //
+ // Deliberately the class directories and not the whole source set
output. A view compiler
+ // registers its own output directory into that output and runs
after the classes task, so
+ // it cannot declare the classes task as its producer without a
cycle, and anything reading
+ // the whole output is left consuming a directory nothing says it
produced. Compiled views
+ // are no use in resolving what a tag library declares anyway.
+
it.generatorClasspath.from(project.configurations.named('compileClasspath'),
classesDirs)
+ it.dependsOn(tasks.named('classes'))
+ }
+ FileCollection packagedSettings =
+
project.files(packagedSettingsDir).builtBy(packageTagLibraryIndex)
+ FileCollection packagedTagLibIndex =
+ project.files(packagedIndexDir).builtBy(packageTagLibraryIndex)
+
+ // Pages resolve tag calls against the index and are compiled in a
process of their own, so the
+ // authoritative index has to be on their classpath.
FileCollection allClasspath =
project.getObjects().fileCollection().from(
[
project.configurations.named('compileClasspath'),
classesDirs,
+ packagedTagLibIndex,
+ packagedSettings,
project.configurations.findByName('providedCompile')
?: null
].findAll { it }
)
- // The Java the rest of the project is built with, so that pages are
built with it too.
- // Absent a toolchain this resolves to the JVM running Gradle, which
is what compiling
- // pages fell back to before and remains the right answer when nothing
else was asked for.
- JavaPluginExtension javaExtension =
project.extensions.getByType(JavaPluginExtension)
- JavaToolchainService toolchains =
project.extensions.getByType(JavaToolchainService)
- Provider<JavaLauncher> launcher =
toolchains.launcherFor(javaExtension.toolchain)
+ // Carried into the artifact and onto the runtime classpath directly
rather than through
+ // processResources, which the classes task waits for - and this waits
for the classes task.
+ if (mainSourceSet != null) {
+ mainSourceSet.runtimeClasspath =
mainSourceSet.runtimeClasspath.plus(packagedTagLibIndex)
Review Comment:
Fixed — the packaged index is added to the `test` and `integrationTest`
runtime classpaths, and `TagLibraryIndexWiringFunctionalSpec` asserts
`TEST_RUNTIME_SEES_PACKAGED=true`. I checked it fails without the change.
##########
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/gsp/GenerateTagLibraryIndexTask.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.gradle.plugin.views.gsp
+
+import javax.inject.Inject
+
+import groovy.transform.CompileStatic
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Classpath
+import org.gradle.api.tasks.IgnoreEmptyDirectories
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.provider.SetProperty
+import org.gradle.api.tasks.Nested
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.jvm.toolchain.JavaLauncher
+import org.gradle.api.tasks.util.PatternSet
+import org.gradle.process.ExecOperations
+import org.gradle.process.JavaExecSpec
+
+/**
+ * Writes the tag library index describing the tag libraries in this project.
+ *
+ * <p>The index has to exist before anything that resolves tag calls is
compiled, which is why this
+ * runs ahead of compilation rather than being produced as a side effect of
it. Generating it for the
+ * whole source set at once is also what lets a renamed or deleted tag library
disappear from it,
+ * where an index accumulated class by class keeps describing tags that no
longer exist.
+ *
+ * <p>The work runs in a forked process against the project's own compile
classpath, because the rules
+ * that decide what a tag is belong to the framework being built rather than
to the build tooling, and
+ * must be the same rules the application applies when it starts.
+ *
+ * @since 8.0.0
+ */
+@CacheableTask
+@CompileStatic
+abstract class GenerateTagLibraryIndexTask extends DefaultTask {
+
+ static final String GENERATOR_CLASS =
'org.grails.taglib.index.TagLibraryIndexGenerator'
+
+ private final ExecOperations execOperations
+
+ @Inject
+ GenerateTagLibraryIndexTask(ExecOperations execOperations) {
+ this.execOperations = execOperations
+ description = 'Generates the tag library index used to resolve tag
calls at compile time'
+ group = 'build'
+ }
+
+ /**
+ * The directories holding tag library sources.
+ *
+ * <p>Defaults to {@code grails-app/taglib}. A project keeping tag
libraries elsewhere can add
+ * those directories, which is what makes them resolvable in the same
compilation that defines
+ * them; without that they are still described as they compile, and so are
resolvable to whatever
+ * is compiled afterwards.
+ */
+ @InputFiles
+ @IgnoreEmptyDirectories
+ @PathSensitive(PathSensitivity.RELATIVE)
+ abstract ConfigurableFileCollection getSourceDirectories()
+
+ /**
+ * The source roots a type this project declares may be resolved from.
+ *
+ * <p>A tag library commonly refers to a service, base class or trait of
the same project, none of
+ * which exist as classes yet. Their source is compiled alongside it so
that what they contribute -
+ * a namespace, tags, a parameter type - is read rather than guessed.
+ */
+ @InputFiles
+ @IgnoreEmptyDirectories
+ @PathSensitive(PathSensitivity.RELATIVE)
+ abstract ConfigurableFileCollection getResolutionSourceRoots()
+
+ /**
+ * Where the index is written. Placed on the compile classpath and
packaged with the artifact.
+ */
+ @OutputDirectory
+ abstract DirectoryProperty getDestinationDirectory()
+
+ /**
+ * Where the settings this build declared are written.
+ *
+ * <p>Kept apart from the descriptors because the two travel differently:
the descriptors are
+ * published, and the settings say how this project is compiled and must
reach no one else. Sharing
+ * a directory would put them wherever the descriptors go, including into
an executable archive
+ * built from the runtime classpath, where no exclusion on an archive task
can reach them.
+ *
+ * <p>Written beside the descriptors when unset, which suits a caller with
nothing to publish.
+ */
+ @OutputDirectory
+ @Optional
+ abstract DirectoryProperty getSettingsDirectory()
+
+ /**
+ * The classpath the generator runs against, which supplies the
framework's discovery rules.
+ */
+ @Classpath
+ abstract ConfigurableFileCollection getGeneratorClasspath()
+
+ /**
+ * Whether this compilation writes parameter names into class files. It
decides whether a tag's
+ * attributes and body parameters have to carry those names to be
dispatchable, so the index must
+ * be generated under the same setting the sources are compiled with.
+ */
+ @Input
+ abstract Property<Boolean> getParameterNamesRetained()
+
+ /**
+ * The source encoding, matching the one compilation uses.
+ */
+ @Input
+ @Optional
+ abstract Property<String> getSourceEncoding()
Review Comment:
Fixed — `sourceEncoding` now has a convention resolved from
`compileGroovy.options.encoding`, set in the `configureEach` block alongside
the other settings both index tasks must agree on.
##########
grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/index/TagLibraryIndexWriter.java:
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.taglib.index;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Writes the compile-time descriptor for a single tag library.
+ *
+ * <p>Two files are produced per tag library: a descriptor named after the tag
library class, and an
+ * entry in a shared {@code index.properties} manifest naming it. The manifest
exists because a
+ * classpath directory cannot be enumerated from inside a jar, so the reader
needs the names up front.
+ * Both live under {@link TagLibraryIndex#INDEX_LOCATION} and merge across
jars without a build step.
+ *
+ * @since 8.0.0
+ */
+public final class TagLibraryIndexWriter {
+
+ private TagLibraryIndexWriter() {
+ }
+
+ /**
+ * Removes any index previously written beneath a directory, so that a
regenerated index describes
+ * only the tag libraries that exist now. Without this a renamed or
deleted tag library would keep
+ * a descriptor, and the manifest naming it, until the build directory was
cleaned.
+ *
+ * @param outputDirectory the directory the index is written beneath
+ * @throws IOException if an existing index cannot be removed
+ */
+ public static void clear(File outputDirectory) throws IOException {
+ if (outputDirectory == null) {
+ return;
+ }
+ File indexDirectory = new File(outputDirectory,
TagLibraryIndex.INDEX_LOCATION);
+ File[] existing = indexDirectory.listFiles();
+ if (existing == null) {
+ return;
+ }
+ for (File file : existing) {
+ if (file.isFile() && file.getName().endsWith(".properties")) {
+ Files.deleteIfExists(file.toPath());
+ }
+ }
+ }
+
+ /**
+ * Writes the descriptor for a tag library into a compiler output
directory.
+ *
+ * @param outputDirectory the compilation target directory; nothing is
written when {@code null}
+ * @param className the binary name of the tag library
+ * @param namespace the namespace the tag library declares
+ * @param tagNames the tag names the tag library declares
+ * @throws IOException if the descriptor cannot be written
+ */
+ public static void write(File outputDirectory, String className, String
namespace,
+ Collection<String> tagNames) throws IOException {
+ Map<String, TagLibraryIndexEntry.Kind> asMethods = new TreeMap<>();
+ for (String tagName : tagNames) {
+ asMethods.put(tagName, TagLibraryIndexEntry.Kind.METHOD);
+ }
+ write(outputDirectory, className, namespace, asMethods);
+ }
+
+ /**
+ * Writes the descriptor for a tag library, recording how each tag is
implemented.
+ *
+ * @param outputDirectory the compilation target directory; nothing is
written when {@code null}
+ * @param className the binary name of the tag library
+ * @param namespace the namespace the tag library declares
+ * @param tags each tag mapped to how it is implemented
+ * @throws IOException if the descriptor cannot be written
+ */
+ public static void write(File outputDirectory, String className, String
namespace,
+ Map<String, TagLibraryIndexEntry.Kind> tags) throws IOException {
+ if (outputDirectory == null || className == null ||
className.isEmpty() ||
+ namespace == null || namespace.isEmpty()) {
+ return;
+ }
+ File indexDirectory = new File(outputDirectory,
TagLibraryIndex.INDEX_LOCATION);
+ if (!indexDirectory.isDirectory() && !indexDirectory.mkdirs() &&
!indexDirectory.isDirectory()) {
+ return;
+ }
+
+ Properties descriptor = new Properties();
+ descriptor.setProperty(TagLibraryIndex.VERSION_KEY,
String.valueOf(TagLibraryIndex.FORMAT_VERSION));
+ descriptor.setProperty(TagLibraryIndex.NAMESPACE_KEY, namespace);
+ descriptor.setProperty(TagLibraryIndex.CLASS_KEY, className);
+ // Sorted so that recompiling unchanged sources produces
byte-identical output, which keeps
+ // the build reproducible and avoids spurious up-to-date checks
failing downstream.
+ // Recorded as "name:KIND" so that a caller can tell a tag it can bind
to from one that has to
+ // be dispatched dynamically, without a second file or a nested format.
+ StringBuilder encoded = new StringBuilder();
+ for (Map.Entry<String, TagLibraryIndexEntry.Kind> tag : new
TreeMap<>(tags).entrySet()) {
+ if (encoded.length() > 0) {
+ encoded.append(',');
+ }
+
encoded.append(tag.getKey()).append(':').append(tag.getValue().name());
+ }
+ descriptor.setProperty(TagLibraryIndex.TAGS_KEY, encoded.toString());
+ store(new File(indexDirectory, className + ".properties"), descriptor);
+
+ File manifest = new File(indexDirectory, "index.properties");
Review Comment:
Both fixed, and the first was worse than "would interleave". I wrote a spec
that writes 32 tag libraries into one directory concurrently, then removed the
guard to check it wasn't vacuous: **29 of the 32 manifest entries were lost.**
Silent, exactly as you say — descriptor on disk, nothing naming it.
Guarded now with a monitor for threads of this JVM and a file lock for a
second process, with `TagLibraryIndexWriterConcurrencySpec` failing without
either.
The stale-entry half is called out in `compiledTags.adoc` next to the
self-describing paragraph.
##########
grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagMethodInvoker.java:
##########
@@ -36,36 +36,25 @@
import groovy.lang.GroovyObject;
import groovy.lang.MissingMethodException;
-import grails.gsp.NotATag;
-import grails.gsp.Tag;
+import org.grails.taglib.discovery.ReflectedTagMethodView;
+import org.grails.taglib.discovery.TagDiscoveryRules;
public final class TagMethodInvoker {
/**
* Method names from framework traits, Spring lifecycle interfaces, and
the like
* that must never be treated as tag methods regardless of the declaring
class.
*/
- private static final Set<String> FRAMEWORK_METHOD_NAMES = Set.of(
- "afterPropertiesSet",
- "currentRequestAttributes",
- "destroy",
- "initializeTagLibrary",
- "onApplicationEvent",
- "raw",
- "throwTagError",
- "withCodec"
- );
-
- private static final Set<String> OBJECT_METHOD_SIGNATURES =
collectSignatures(Object.class);
- private static final Set<String> GROOVY_OBJECT_METHOD_SIGNATURES =
collectSignatures(GroovyObject.class);
-
- private static Set<String> collectSignatures(Class<?> type) {
- Set<String> signatures = new HashSet<>();
- for (Method method : type.getMethods()) {
- signatures.add(signature(method));
- }
- return Collections.unmodifiableSet(signatures);
- }
+ /**
Review Comment:
Removed.
##########
grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/discovery/TagLibraryAstDiscovery.java:
##########
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.taglib.discovery;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.Closure;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+
+import org.grails.taglib.index.TagLibraryIndexEntry;
+
+/**
+ * Reads a tag library's namespace and tag names from its syntax tree.
+ *
+ * <p>Classification is delegated to {@link TagDiscoveryRules}, the same rules
an application applies
+ * when it registers tag libraries, so the two cannot disagree about what a
tag is. What remains here
+ * is reading the namespace and gathering the candidate members from the tree.
+ *
+ * @since 8.0.0
+ */
+public final class TagLibraryAstDiscovery {
+
+ public static final String DEFAULT_NAMESPACE = "g";
+
+ private static final String NAMESPACE_FIELD = "namespace";
+
+ private static final ClassNode CLOSURE_TYPE =
ClassHelper.make(Closure.class);
+
+ private TagLibraryAstDiscovery() {
+ }
+
+ /**
+ * Resolves the namespace the way {@code DefaultGrailsTagLibClass} does at
runtime, which reads the
+ * static {@code namespace} property through the class hierarchy.
+ *
+ * @param classNode the tag library
+ * @return the namespace, or {@code null} when it cannot be determined
without running the code, in
+ * which case no descriptor should be written and the tag library
resolves dynamically
+ */
+ public static String resolveNamespace(ClassNode classNode) {
+ for (ClassNode current = classNode; current != null &&
!ClassHelper.isObjectType(current);
+ current = current.getSuperClass()) {
+ FieldNode namespaceField =
current.getDeclaredField(NAMESPACE_FIELD);
+ if (namespaceField == null || !namespaceField.isStatic()) {
+ continue;
+ }
+ Expression initial = namespaceField.getInitialExpression();
+ if (initial instanceof ConstantExpression constant &&
constant.getValue() != null) {
+ String value = constant.getValue().toString().trim();
+ return value.isEmpty() ? DEFAULT_NAMESPACE : value;
+ }
+ // Declared, but its value is only known once the initialiser runs
- a reference to a shared
+ // constant, a concatenation, and so on. Guessing "g" here would
file the tags under the
+ // wrong namespace, so the tag library is left out of the index
entirely.
+ return null;
+ }
+ return DEFAULT_NAMESPACE;
+ }
+
+ /**
+ * @param classNode the tag library
+ * @param parameterNamesRetained whether this compilation writes parameter
names into the class file
+ * @return every tag the library declares, whether as a tag method or a
legacy closure field
+ */
+ /**
Review Comment:
Both removed — the stale javadoc block, and `findTagNames`, which had no
caller anywhere.
##########
grails-gsp/grails-taglib/src/main/groovy/org/grails/taglib/TagLibraryMetaUtils.groovy:
##########
@@ -32,6 +32,18 @@ import grails.core.gsp.GrailsTagLibClass
import grails.util.GrailsClassUtils
import org.grails.taglib.encoder.OutputContextLookupHelper
+/**
+ * Installs tags onto metaclasses.
+ *
+ * <p>Tags are resolved through {@link TagLibraryLookup} and invoked through
+ * {@link CompiledTagInvocation}, so nothing needs installing onto a metaclass
to call a tag. What
+ * remains here is the dynamic dispatch that a tag library registered at
runtime still relies on,
+ * reachable through {@code methodMissingForTagLib} with metaclass
installation switched off.
+ *
+ * @deprecated Installing tags onto metaclasses is no longer part of
dispatching a tag. Resolve
+ * through {@link TagLibraryLookup} and invoke through {@link
CompiledTagInvocation}.
+ */
+@Deprecated
Review Comment:
Agreed, and changed. `@Deprecated` moved off the class onto the installing
methods (`enhanceTagLibMetaClass`, `registerTagMetaMethods`,
`registerMethodMissingForTags`, `registerNamespaceMetaProperties`,
`registerNamespaceMetaProperty`, `registerPropertyMissingForTag`,
`addTagLibMethodToMetaClass`), each with `since = '8.0.0'`.
`methodMissingForTagLib` is not deprecated — it's the dispatch path, and this
change increases its use.
--
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]