55 */
66package dev .chojo .ocular .processor ;
77
8- import dev .chojo .ocular .override .OverwritePrefix ;
98import dev .chojo .ocular .override .Overwrite ;
9+ import dev .chojo .ocular .override .OverwritePrefix ;
1010
1111import javax .annotation .processing .AbstractProcessor ;
1212import javax .annotation .processing .Filer ;
2323import javax .lang .model .element .PackageElement ;
2424import javax .lang .model .element .TypeElement ;
2525import javax .tools .Diagnostic ;
26+ import javax .tools .FileObject ;
2627import javax .tools .JavaFileObject ;
28+ import javax .tools .StandardLocation ;
2729import java .io .IOException ;
30+ import java .io .Writer ;
2831import java .util .ArrayList ;
2932import java .util .HashMap ;
33+ import java .util .LinkedHashMap ;
3034import java .util .List ;
3135import java .util .Map ;
3236import java .util .Set ;
@@ -125,16 +129,30 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
125129 classesToProcess .computeIfAbsent (enclosingClass , k -> new ArrayList <>()).add (element );
126130 }
127131
132+ // Collect override info for the reference file
133+ Map <String , List <OverrideInfo >> allOverrides = new LinkedHashMap <>();
134+
128135 // Generate one override provider class per config class
129136 for (Map .Entry <TypeElement , List <Element >> entry : classesToProcess .entrySet ()) {
130137 try {
131- generateOverrideProvider (entry .getKey (), entry .getValue ());
138+ List <OverrideInfo > infos = generateOverrideProvider (entry .getKey (), entry .getValue ());
139+ allOverrides .put (entry .getKey ().getQualifiedName ().toString (), infos );
132140 } catch (IOException e ) {
133141 messager .printMessage (Diagnostic .Kind .ERROR ,
134142 "Could not generate override provider for " + entry .getKey ().getQualifiedName () + ": " + e .getMessage ());
135143 }
136144 }
137145
146+ // Generate reference documentation file
147+ if (!allOverrides .isEmpty ()) {
148+ try {
149+ generateOverrideDocumentation (allOverrides );
150+ } catch (IOException e ) {
151+ messager .printMessage (Diagnostic .Kind .ERROR ,
152+ "Could not generate override documentation: " + e .getMessage ());
153+ }
154+ }
155+
138156 return true ;
139157 }
140158
@@ -148,7 +166,41 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
148166 * <li>A {@code getValue()} method that looks up a field name in the map.</li>
149167 * </ul>
150168 */
151- private void generateOverrideProvider (TypeElement typeElement , List <Element > elements ) throws IOException {
169+ /**
170+ * Holds information about a single override for documentation and logging purposes.
171+ */
172+ private record OverrideInfo (String fieldName , String description , List <String > sources ) {
173+ }
174+
175+ /**
176+ * Generates a Markdown reference file at {@code META-INF/ocular/overrides.md} listing all
177+ * overridable properties and environment variables with their descriptions.
178+ */
179+ private void generateOverrideDocumentation (Map <String , List <OverrideInfo >> allOverrides ) throws IOException {
180+ FileObject resource = filer .createResource (StandardLocation .CLASS_OUTPUT , "" , "META-INF/ocular/overrides.md" );
181+ try (Writer writer = resource .openWriter ()) {
182+ writer .append ("# Ocular Override Reference\n \n " );
183+ writer .append ("This file is auto-generated by the Ocular annotation processor.\n " );
184+ writer .append ("It lists all available configuration overrides.\n \n " );
185+
186+ for (Map .Entry <String , List <OverrideInfo >> entry : allOverrides .entrySet ()) {
187+ writer .append ("## " ).append (entry .getKey ()).append ("\n \n " );
188+ for (OverrideInfo info : entry .getValue ()) {
189+ writer .append ("### " ).append (info .fieldName ()).append ("\n \n " );
190+ if (!info .description ().isEmpty ()) {
191+ writer .append (info .description ()).append ("\n \n " );
192+ }
193+ writer .append ("Sources (in priority order):\n " );
194+ for (String source : info .sources ()) {
195+ writer .append ("- " ).append (source ).append ("\n " );
196+ }
197+ writer .append ("\n " );
198+ }
199+ }
200+ }
201+ }
202+
203+ private List <OverrideInfo > generateOverrideProvider (TypeElement typeElement , List <Element > elements ) throws IOException {
152204 // Extract the package (e.g. "com.example") so we can put the generated file in the same package
153205 String packageName = ((PackageElement ) typeElement .getEnclosingElement ()).getQualifiedName ().toString ();
154206 // Full name like "com.example.MyConfig"
@@ -170,6 +222,9 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
170222
171223 // Ask the compiler to create a new .java source file. The compiler will automatically
172224 // compile this generated file in the same compilation round — we just need to write valid Java into it.
225+ // Collect override info for documentation
226+ List <OverrideInfo > overrideInfos = new ArrayList <>();
227+
173228 JavaFileObject builderFile = filer .createSourceFile (packageName + "." + generatedClassName );
174229 // SourceWriter is a helper that handles indentation so the generated code is readable
175230 try (SourceWriter out = new SourceWriter (builderFile .openWriter ())) {
@@ -191,7 +246,10 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
191246 for (Element element : elements ) {
192247 String fieldName = element .getSimpleName ().toString ();
193248 // Write the lookup code for this field's env/prop sources into the constructor
194- emitLookupsInOrder (out , element , prefix , forcePrefix , fieldName );
249+ OverrideInfo info = emitLookupsInOrder (out , element , prefix , forcePrefix , fieldName );
250+ if (info != null ) {
251+ overrideInfos .add (info );
252+ }
195253 }
196254 out .endBlock ();
197255
@@ -203,6 +261,7 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
203261
204262 out .endBlock ();
205263 }
264+ return overrideInfos ;
206265 }
207266
208267 /**
@@ -217,12 +276,17 @@ private void generateOverrideProvider(TypeElement typeElement, List<Element> ele
217276 * code structure (mirrors), not the actual runtime annotation objects. Mirrors preserve the
218277 * declaration order of attributes, which is essential for correct precedence.
219278 */
220- private void emitLookupsInOrder (SourceWriter out , Element element , String prefix , boolean forcePrefix , String fieldName ) throws IOException {
279+ private OverrideInfo emitLookupsInOrder (SourceWriter out , Element element , String prefix , boolean forcePrefix , String fieldName ) throws IOException {
221280 // Get the compile-time representation ("mirror") of the @Overwrite annotation on this field.
222281 // We need the mirror (not the annotation object) because at compile time the annotation class
223282 // isn't loaded as a real object — it only exists as metadata in the source code.
224283 AnnotationMirror overwriteMirror = findOverwriteMirror (element );
225- if (overwriteMirror == null ) return ;
284+ if (overwriteMirror == null ) return null ;
285+
286+ // Extract the description from the @Overwrite annotation
287+ String description = extractStringValue (overwriteMirror , "description" );
288+ if (description == null ) description = "" ;
289+ List <String > sources = new ArrayList <>();
226290
227291 // Walk through each attribute of @Overwrite in the order the user wrote them.
228292 // For example, in @Overwrite(prop = @Prop(), env = @Env()), we'd iterate:
@@ -232,6 +296,8 @@ private void emitLookupsInOrder(SourceWriter out, Element element, String prefix
232296 // The first source that provides a non-null value wins.
233297 for (Map .Entry <? extends ExecutableElement , ? extends AnnotationValue > entry :
234298 overwriteMirror .getElementValues ().entrySet ()) {
299+ // Skip the description value
300+ if (entry .getValue ().getValue ().getClass () == String .class ) continue ;
235301 // attrName is either "prop" or "env" — the attribute name from @Overwrite
236302 String attrName = entry .getKey ().getSimpleName ().toString ();
237303 // The value is an array of nested annotations (e.g. the @Prop[] or @Env[] array)
@@ -255,6 +321,7 @@ private void emitLookupsInOrder(SourceWriter out, Element element, String prefix
255321 }
256322 // Write Java code like: String value = System.getProperty("myclass.host");
257323 emitLookup (out , fieldName , "System.getProperty(\" " + key + "\" )" );
324+ sources .add ("Property: `" + key + "`" );
258325 }
259326 } else if ("env" .equals (attrName )) {
260327 prefix = prefix .replace ("." , "_" ).toUpperCase ();
@@ -271,9 +338,11 @@ private void emitLookupsInOrder(SourceWriter out, Element element, String prefix
271338 }
272339 // Write Java code like: String value = System.getenv("MYCLASS_HOST");
273340 emitLookup (out , fieldName , "System.getenv(\" " + key + "\" )" );
341+ sources .add ("Environment: `" + key + "`" );
274342 }
275343 }
276344 }
345+ return new OverrideInfo (fieldName , description , sources );
277346 }
278347
279348 /**
0 commit comments