44import com .sun .source .tree .IdentifierTree ;
55import com .sun .source .tree .ImportTree ;
66import com .sun .source .tree .LiteralTree ;
7+ import com .sun .source .tree .LambdaExpressionTree ;
78import com .sun .source .tree .MemberSelectTree ;
9+ import com .sun .source .tree .MethodTree ;
810import com .sun .source .tree .MethodInvocationTree ;
911import com .sun .source .tree .Tree ;
12+ import com .sun .source .tree .VariableTree ;
1013import com .sun .source .util .JavacTask ;
1114import com .sun .source .util .SourcePositions ;
15+ import com .sun .source .util .TreePath ;
1216import com .sun .source .util .TreePathScanner ;
17+ import com .sun .source .util .TreeScanner ;
1318import com .sun .source .util .Trees ;
1419import java .io .IOException ;
1520import java .nio .charset .StandardCharsets ;
@@ -62,11 +67,28 @@ public final class BpjSourceTransformer {
6267 * @return transformation result
6368 */
6469 public TransformationResult transform (Path sourcePath , String source ) {
70+ return transform (sourcePath , source , false );
71+ }
72+
73+ /**
74+ * Transforms one Java source file content.
75+ *
76+ * @param sourcePath source path used in parser diagnostics
77+ * @param source source code content
78+ * @param failOnUnresolved whether unresolved placeholder roots should fail transformation
79+ * @return transformation result
80+ */
81+ public TransformationResult transform (Path sourcePath , String source , boolean failOnUnresolved ) {
6582 Objects .requireNonNull (sourcePath , "sourcePath cannot be null" );
6683 Objects .requireNonNull (source , "source cannot be null" );
6784
6885 ParseContext parse = parse (sourcePath , source );
69- List <Insertion > insertions = collectInsertions (sourcePath , parse .compilationUnit , parse .sourcePositions );
86+ List <Insertion > insertions = collectInsertions (
87+ sourcePath ,
88+ parse .compilationUnit ,
89+ parse .sourcePositions ,
90+ failOnUnresolved
91+ );
7092 if (insertions .isEmpty ()) {
7193 return new TransformationResult (source , 0 );
7294 }
@@ -123,14 +145,23 @@ private void failIfParseErrors(Path sourcePath, DiagnosticCollector<JavaFileObje
123145 private List <Insertion > collectInsertions (
124146 Path sourcePath ,
125147 CompilationUnitTree compilationUnit ,
126- SourcePositions sourcePositions
148+ SourcePositions sourcePositions ,
149+ boolean failOnUnresolved
127150 ) {
128151 List <Insertion > insertions = new ArrayList <>();
129152
130153 new TreePathScanner <Void , Void >() {
131154 @ Override
132155 public Void visitMethodInvocation (MethodInvocationTree invocation , Void unused ) {
133- maybeCollect (sourcePath , invocation , compilationUnit , sourcePositions , insertions );
156+ maybeCollect (
157+ sourcePath ,
158+ getCurrentPath (),
159+ invocation ,
160+ compilationUnit ,
161+ sourcePositions ,
162+ insertions ,
163+ failOnUnresolved
164+ );
134165 return super .visitMethodInvocation (invocation , unused );
135166 }
136167 }.scan (compilationUnit , null );
@@ -140,10 +171,12 @@ public Void visitMethodInvocation(MethodInvocationTree invocation, Void unused)
140171
141172 private void maybeCollect (
142173 Path sourcePath ,
174+ TreePath invocationPath ,
143175 MethodInvocationTree invocation ,
144176 CompilationUnitTree compilationUnit ,
145177 SourcePositions sourcePositions ,
146- List <Insertion > insertions
178+ List <Insertion > insertions ,
179+ boolean failOnUnresolved
147180 ) {
148181 if (invocation .getArguments ().size () != 1 ) {
149182 return ;
@@ -173,6 +206,19 @@ private void maybeCollect(
173206 return ;
174207 }
175208
209+ if (failOnUnresolved ) {
210+ List <String > unresolved = findUnresolvedRoots (roots , invocationPath );
211+ if (!unresolved .isEmpty ()) {
212+ throw unresolvedPlaceholderException (
213+ sourcePath ,
214+ compilationUnit ,
215+ sourcePositions ,
216+ invocation ,
217+ unresolved
218+ );
219+ }
220+ }
221+
176222 long end = sourcePositions .getEndPosition (compilationUnit , argument );
177223 if (end < 0 ) {
178224 return ;
@@ -257,6 +303,77 @@ private IllegalArgumentException invalidPlaceholderException(
257303 return new IllegalArgumentException (message );
258304 }
259305
306+ private List <String > findUnresolvedRoots (LinkedHashSet <String > roots , TreePath invocationPath ) {
307+ Set <String > availableRoots = collectAvailableRoots (invocationPath );
308+ return roots .stream ()
309+ .filter (root -> !availableRoots .contains (root ))
310+ .toList ();
311+ }
312+
313+ private Set <String > collectAvailableRoots (TreePath invocationPath ) {
314+ LinkedHashSet <String > names = new LinkedHashSet <>();
315+ names .add ("this" );
316+ names .add ("super" );
317+
318+ for (TreePath current = invocationPath ; current != null ; current = current .getParentPath ()) {
319+ Tree leaf = current .getLeaf ();
320+ if (leaf instanceof MethodTree method ) {
321+ for (VariableTree parameter : method .getParameters ()) {
322+ names .add (parameter .getName ().toString ());
323+ }
324+ if (method .getBody () != null ) {
325+ collectVariableNames (method .getBody (), names );
326+ }
327+ } else if (leaf instanceof LambdaExpressionTree lambda ) {
328+ for (VariableTree parameter : lambda .getParameters ()) {
329+ names .add (parameter .getName ().toString ());
330+ }
331+ collectVariableNames (lambda .getBody (), names );
332+ } else if (leaf instanceof com .sun .source .tree .ClassTree classTree ) {
333+ for (Tree member : classTree .getMembers ()) {
334+ if (member instanceof VariableTree field ) {
335+ names .add (field .getName ().toString ());
336+ }
337+ }
338+ }
339+ }
340+
341+ return names ;
342+ }
343+
344+ private void collectVariableNames (Tree tree , Set <String > names ) {
345+ new TreeScanner <Void , Set <String >>() {
346+ @ Override
347+ public Void visitClass (com .sun .source .tree .ClassTree node , Set <String > collector ) {
348+ return null ;
349+ }
350+
351+ @ Override
352+ public Void visitVariable (VariableTree variable , Set <String > collector ) {
353+ collector .add (variable .getName ().toString ());
354+ return super .visitVariable (variable , collector );
355+ }
356+ }.scan (tree , names );
357+ }
358+
359+ private IllegalArgumentException unresolvedPlaceholderException (
360+ Path sourcePath ,
361+ CompilationUnitTree compilationUnit ,
362+ SourcePositions sourcePositions ,
363+ MethodInvocationTree invocation ,
364+ List <String > unresolved
365+ ) {
366+ long start = sourcePositions .getStartPosition (compilationUnit , invocation );
367+ long line = start >= 0 ? compilationUnit .getLineMap ().getLineNumber (start ) : -1 ;
368+ String location = line > 0 ? sourcePath + ":" + line : sourcePath .toString ();
369+
370+ String message = "Unresolved BPJ placeholder root(s) " + unresolved
371+ + " at " + location
372+ + ". Define these variables in scope or disable this validation with "
373+ + "<failOnUnresolved>false</failOnUnresolved>." ;
374+ return new IllegalArgumentException (message );
375+ }
376+
260377 private String escapeBraces (String template ) {
261378 return template
262379 .replace ("{{" , ESCAPED_OPEN_TOKEN )
0 commit comments