@@ -506,7 +506,7 @@ Version {{name}}
506506 {{#commits}}
507507 {{#ifMatches messageTitle "^(?!Merge).*"}}
508508 * **{{{messageTitle}}}**
509-
509+
510510 {{authorName}}, {{commitDate}}
511511 {{/ifMatches}}
512512 {{/commits}}
@@ -717,4 +717,126 @@ jmh {
717717 fork = 3
718718 iterations = 5
719719 timeOnIteration = ' 1s'
720- }
720+ }
721+
722+ // ---------------------------------------------------------------------------------------------
723+ // Grammar ambiguity gate
724+ //
725+ // JavaCC reports choice conflicts as "Warning: ..." on stdout and still exits 0, so an ambiguity
726+ // introduced by a PR is completely invisible to `./gradlew check`. The `org.javacc.javacc` plugin
727+ // forwards that output to the Gradle logger, where it cannot be inspected either.
728+ //
729+ // This task therefore re-runs JavaCC on the JJTree output, into a throwaway directory, captures
730+ // stdout+stderr and fails when any warning survives the allow list.
731+ // ---------------------------------------------------------------------------------------------
732+
733+ // Regexes for warnings that are known-benign. Add an entry ONLY together with a comment saying
734+ // why. Never add a "Choice conflict" pattern here — that is the thing this gate exists to catch.
735+ def grammarWarningAllowList = [
736+ // JJTree copies the options block into the .jj; re-passing -OUTPUT_DIRECTORY on the
737+ // command line makes JavaCC complain that it overrides the in-file value.
738+ ~/ ^Warning: .*Command line setting of .* modifies option value in file\. /
739+ ]
740+
741+ def javaccMainClassOverride = project. findProperty(' javaccMainClass' )
742+
743+ tasks. register(' checkGrammarAmbiguity' ) {
744+ group = ' verification'
745+ description = ' Fails the build when JavaCC reports choice conflicts (grammar ambiguities).'
746+
747+ dependsOn(compileJavacc)
748+
749+ def jjFile = layout. buildDirectory
750+ .file(' generated/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jj' )
751+ def scratchDir = layout. buildDirectory. dir(' grammar-check' )
752+ def reportFile = layout. buildDirectory. file(' reports/grammar/javacc-warnings.txt' )
753+ def javaccClasspath = configurations. javacc
754+ def allowList = grammarWarningAllowList
755+ def mainClassOverride = javaccMainClassOverride
756+
757+ inputs. file(jjFile)
758+ inputs. files(javaccClasspath)
759+ outputs. file(reportFile)
760+
761+ doLast {
762+ def jj = jjFile. get(). asFile
763+ if (! jj. exists()) {
764+ throw new GradleException (" Generated grammar not found: ${ jj} — did compileJavacc run?" )
765+ }
766+
767+ def out = scratchDir. get(). asFile
768+ out. deleteDir()
769+ out. mkdirs()
770+
771+ // JavaCC 8 splits into javacc-core / javacc-java; take Main-Class from whichever jar
772+ // declares one, so this keeps working across upgrades of the Manticore JavaCC branch.
773+ def mainClass = mainClassOverride ?: javaccClasspath. files. findResult { f ->
774+ if (! f. name. endsWith(' .jar' )) {
775+ return null
776+ }
777+ new java.util.jar.JarFile (f). withCloseable { jar ->
778+ jar. manifest?. mainAttributes?. getValue(' Main-Class' )
779+ }
780+ } ?: ' org.javacc.parser.Main'
781+
782+ def isWindows = System . getProperty(' os.name' ). toLowerCase(Locale . ROOT ). contains(' win' )
783+ def javaExe = new File (new File (System . getProperty(' java.home' ), ' bin' ),
784+ isWindows ? ' java.exe' : ' java' )
785+
786+ def proc = new ProcessBuilder (
787+ javaExe. absolutePath,
788+ ' -cp' , javaccClasspath. asPath,
789+ mainClass. toString(),
790+ " -OUTPUT_DIRECTORY=${ out.absolutePath} " . toString(),
791+ jj. absolutePath)
792+ .directory(out)
793+ .redirectErrorStream(true )
794+ .start()
795+
796+ def output = proc. inputStream. getText(' UTF-8' )
797+ def exit = proc. waitFor()
798+
799+ // Warnings are multi-line: a "Warning:" header followed by indented continuation lines.
800+ def warnings = []
801+ def current = null
802+ output. eachLine { line ->
803+ if (line. startsWith(' Warning:' )) {
804+ current = new StringBuilder (line)
805+ warnings << current
806+ } else if (current != null && line. startsWith(' ' )) {
807+ current. append(' \n ' ). append(line)
808+ } else {
809+ current = null
810+ }
811+ }
812+
813+ def offending = warnings
814+ .collect { it. toString() }
815+ .findAll { w -> allowList. every { ! (w =~ it) } }
816+
817+ def report = reportFile. get(). asFile
818+ report. parentFile. mkdirs()
819+ report. setText(offending. isEmpty() ? ' No JavaCC grammar warnings.\n '
820+ : offending. join(' \n\n ' ) + ' \n ' , ' UTF-8' )
821+
822+ if (exit != 0 ) {
823+ logger. error(output)
824+ throw new GradleException (" JavaCC exited with ${ exit} while analysing the grammar." )
825+ }
826+
827+ if (! offending. isEmpty()) {
828+ def conflicts = offending. count { it. contains(' Choice conflict' ) }
829+ logger. error(" \n ${ offending.join('\n\n')} \n " )
830+ throw new GradleException (
831+ " ${ offending.size()} JavaCC grammar warning(s), ${ conflicts} of them choice " +
832+ " conflicts. Resolve the ambiguity or add an explicit LOOKAHEAD to the earlier " +
833+ " expansion. Report: ${ report} " )
834+ }
835+
836+ logger. lifecycle(' Grammar is free of JavaCC choice conflicts.' )
837+ }
838+ }
839+
840+ tasks. named(' check' ) {
841+ dependsOn(' checkGrammarAmbiguity' )
842+ }
0 commit comments