@@ -681,3 +681,239 @@ func TestConvertDependenciesToPatches(t *testing.T) {
681681 })
682682 }
683683}
684+
685+ // pomWithParentAndProp returns a parent POM that declares a property.
686+ func pomWithParentAndProp (propName , propValue string ) string {
687+ return `<?xml version="1.0" encoding="UTF-8"?>
688+ <project xmlns="http://maven.apache.org/POM/4.0.0">
689+ <modelVersion>4.0.0</modelVersion>
690+ <groupId>com.example</groupId>
691+ <artifactId>parent</artifactId>
692+ <version>1.0.0</version>
693+ <packaging>pom</packaging>
694+ <properties>
695+ <` + propName + `>` + propValue + `</` + propName + `>
696+ </properties>
697+ </project>`
698+ }
699+
700+ // pomWithRelativeParent returns a POM that points at a parent via <relativePath>.
701+ func pomWithRelativeParent (relativePath string ) string {
702+ return `<?xml version="1.0" encoding="UTF-8"?>
703+ <project xmlns="http://maven.apache.org/POM/4.0.0">
704+ <modelVersion>4.0.0</modelVersion>
705+ <parent>
706+ <groupId>com.example</groupId>
707+ <artifactId>parent</artifactId>
708+ <version>1.0.0</version>
709+ <relativePath>` + relativePath + `</relativePath>
710+ </parent>
711+ <groupId>com.example</groupId>
712+ <artifactId>child</artifactId>
713+ <version>1.0.0</version>
714+ </project>`
715+ }
716+
717+ // TestAnalyze_PropertySources_SinglePom checks that properties defined in the
718+ // analysed pom.xml itself are attributed to that file.
719+ func TestAnalyze_PropertySources_SinglePom (t * testing.T ) {
720+ dir := t .TempDir ()
721+ pomContent := `<?xml version="1.0" encoding="UTF-8"?>
722+ <project xmlns="http://maven.apache.org/POM/4.0.0">
723+ <modelVersion>4.0.0</modelVersion>
724+ <groupId>com.example</groupId>
725+ <artifactId>test</artifactId>
726+ <version>1.0.0</version>
727+ <properties>
728+ <netty.version>4.1.100.Final</netty.version>
729+ </properties>
730+ </project>`
731+ pomPath := filepath .Join (dir , "pom.xml" )
732+ writeFile (t , pomPath , pomContent )
733+
734+ ma := & MavenAnalyzer {}
735+ result , err := ma .Analyze (t .Context (), pomPath )
736+ if err != nil {
737+ t .Fatalf ("Analyze: %v" , err )
738+ }
739+
740+ if src := result .PropertySources ["netty.version" ]; src != "pom.xml" {
741+ t .Errorf ("PropertySources[netty.version] = %q, want %q" , src , "pom.xml" )
742+ }
743+ }
744+
745+ // TestAnalyze_PropertySources_DirectoryAnalysis checks that analyzeAllPoms
746+ // attributes each property to the POM file that declares it.
747+ func TestAnalyze_PropertySources_DirectoryAnalysis (t * testing.T ) {
748+ dir := t .TempDir ()
749+ writeFile (t , filepath .Join (dir , "pom.xml" ), `<?xml version="1.0" encoding="UTF-8"?>
750+ <project xmlns="http://maven.apache.org/POM/4.0.0">
751+ <modelVersion>4.0.0</modelVersion>
752+ <groupId>com.example</groupId><artifactId>root</artifactId><version>1.0.0</version>
753+ <properties><root.prop>1.0</root.prop></properties>
754+ </project>` )
755+ writeFile (t , filepath .Join (dir , "module-a" , "pom.xml" ), `<?xml version="1.0" encoding="UTF-8"?>
756+ <project xmlns="http://maven.apache.org/POM/4.0.0">
757+ <modelVersion>4.0.0</modelVersion>
758+ <groupId>com.example</groupId><artifactId>module-a</artifactId><version>1.0.0</version>
759+ <properties><module.prop>2.0</module.prop></properties>
760+ </project>` )
761+
762+ ma := & MavenAnalyzer {}
763+ result , err := ma .Analyze (t .Context (), dir )
764+ if err != nil {
765+ t .Fatalf ("Analyze: %v" , err )
766+ }
767+
768+ if src := result .PropertySources ["root.prop" ]; src != "pom.xml" {
769+ t .Errorf ("PropertySources[root.prop] = %q, want %q" , src , "pom.xml" )
770+ }
771+ if src := result .PropertySources ["module.prop" ]; src != filepath .Join ("module-a" , "pom.xml" ) {
772+ t .Errorf ("PropertySources[module.prop] = %q, want %q" , src , filepath .Join ("module-a" , "pom.xml" ))
773+ }
774+ }
775+
776+ // TestAnalyze_PropertySources_ParentPomOutsideTree checks that a property
777+ // declared in a parent POM referenced via <parent><relativePath> (potentially
778+ // outside the project root) is found and attributed correctly.
779+ func TestAnalyze_PropertySources_ParentPomOutsideTree (t * testing.T ) {
780+ root := t .TempDir ()
781+ // Project layout: root/lib/pom.xml → parent is root/pom.xml
782+ parentPom := filepath .Join (root , "pom.xml" )
783+ childPom := filepath .Join (root , "lib" , "pom.xml" )
784+
785+ writeFile (t , parentPom , pomWithParentAndProp ("netty.version" , "4.1.100.Final" ))
786+ writeFile (t , childPom , pomWithRelativeParent ("../pom.xml" ))
787+
788+ ma := & MavenAnalyzer {}
789+ result , err := ma .Analyze (t .Context (), childPom )
790+ if err != nil {
791+ t .Fatalf ("Analyze: %v" , err )
792+ }
793+
794+ if v := result .Properties ["netty.version" ]; v != "4.1.100.Final" {
795+ t .Errorf ("Properties[netty.version] = %q, want 4.1.100.Final" , v )
796+ }
797+ if src := result .PropertySources ["netty.version" ]; src != "../pom.xml" {
798+ t .Errorf ("PropertySources[netty.version] = %q, want %q" , src , "../pom.xml" )
799+ }
800+ }
801+
802+ // TestAnalyze_PropertySources_ParentPomOutsideTree_Directory checks the same
803+ // for directory-mode analysis (analyzeAllPoms path).
804+ func TestAnalyze_PropertySources_ParentPomOutsideTree_Directory (t * testing.T ) {
805+ root := t .TempDir ()
806+ parentPom := filepath .Join (root , "pom.xml" )
807+ childDir := filepath .Join (root , "lib" )
808+ childPom := filepath .Join (childDir , "pom.xml" )
809+
810+ writeFile (t , parentPom , pomWithParentAndProp ("netty.version" , "4.1.100.Final" ))
811+ // Child POM references a dep via the property so PropertyUsage is populated.
812+ writeFile (t , childPom , `<?xml version="1.0" encoding="UTF-8"?>
813+ <project xmlns="http://maven.apache.org/POM/4.0.0">
814+ <modelVersion>4.0.0</modelVersion>
815+ <parent>
816+ <groupId>com.example</groupId>
817+ <artifactId>parent</artifactId>
818+ <version>1.0.0</version>
819+ <relativePath>../pom.xml</relativePath>
820+ </parent>
821+ <groupId>com.example</groupId>
822+ <artifactId>child</artifactId>
823+ <version>1.0.0</version>
824+ <dependencyManagement>
825+ <dependencies>
826+ <dependency>
827+ <groupId>io.netty</groupId>
828+ <artifactId>netty-all</artifactId>
829+ <version>${netty.version}</version>
830+ </dependency>
831+ </dependencies>
832+ </dependencyManagement>
833+ </project>` )
834+
835+ ma := & MavenAnalyzer {}
836+ result , err := ma .Analyze (t .Context (), childDir )
837+ if err != nil {
838+ t .Fatalf ("Analyze: %v" , err )
839+ }
840+
841+ if v := result .Properties ["netty.version" ]; v != "4.1.100.Final" {
842+ t .Errorf ("Properties[netty.version] = %q, want 4.1.100.Final" , v )
843+ }
844+ if src := result .PropertySources ["netty.version" ]; src != "../pom.xml" {
845+ t .Errorf ("PropertySources[netty.version] = %q, want %q" , src , "../pom.xml" )
846+ }
847+ }
848+
849+ // TestMergeProperty verifies first-definition-wins and no double-assignment.
850+ func TestMergeProperty (t * testing.T ) {
851+ result := & analyzer.AnalysisResult {
852+ Properties : make (map [string ]string ),
853+ PropertySources : make (map [string ]string ),
854+ }
855+
856+ if ! mergeProperty (t .Context (), result , "k" , "v1" , "a.xml" ) {
857+ t .Error ("first merge should return true (newly added)" )
858+ }
859+ if result .Properties ["k" ] != "v1" || result .PropertySources ["k" ] != "a.xml" {
860+ t .Errorf ("property not set correctly after first merge" )
861+ }
862+
863+ // Same value — no warning, returns false.
864+ if mergeProperty (t .Context (), result , "k" , "v1" , "b.xml" ) {
865+ t .Error ("merge of same value should return false (already present)" )
866+ }
867+ if result .PropertySources ["k" ] != "a.xml" {
868+ t .Error ("source should not change when property already present" )
869+ }
870+
871+ // Different value — conflict warning, still returns false, source unchanged.
872+ if mergeProperty (t .Context (), result , "k" , "v2" , "c.xml" ) {
873+ t .Error ("conflicting merge should return false (already present)" )
874+ }
875+ if result .Properties ["k" ] != "v1" {
876+ t .Error ("conflicting value should not overwrite existing" )
877+ }
878+ }
879+
880+ // TestResolveUnknownProperties_ParentChain verifies that properties missing
881+ // from the scanned files are found by following <parent><relativePath>.
882+ func TestResolveUnknownProperties_ParentChain (t * testing.T ) {
883+ root := t .TempDir ()
884+ parentPom := filepath .Join (root , "pom.xml" )
885+ childPom := filepath .Join (root , "lib" , "pom.xml" )
886+
887+ writeFile (t , parentPom , pomWithParentAndProp ("netty.version" , "4.1.100.Final" ))
888+ writeFile (t , childPom , pomWithRelativeParent ("../pom.xml" ))
889+
890+ usage := map [string ]int {"netty.version" : 1 , "already.found" : 1 }
891+ known := map [string ]string {"already.found" : "1.0" } // pre-filled, should be skipped
892+
893+ results := resolveUnknownProperties (t .Context (), usage , known , childPom , filepath .Join (root , "lib" ))
894+
895+ if len (results ) != 1 {
896+ t .Fatalf ("expected 1 result, got %d" , len (results ))
897+ }
898+ pf := results [0 ]
899+ if v := pf .Properties ["netty.version" ]; v != "4.1.100.Final" {
900+ t .Errorf ("Properties[netty.version] = %q, want 4.1.100.Final" , v )
901+ }
902+ if pf .PomFile != "../pom.xml" {
903+ t .Errorf ("PomFile = %q, want %q" , pf .PomFile , "../pom.xml" )
904+ }
905+ }
906+
907+ // TestResolveUnknownProperties_NotFound verifies that a property absent from
908+ // the entire parent chain returns no results (not an error).
909+ func TestResolveUnknownProperties_NotFound (t * testing.T ) {
910+ dir := t .TempDir ()
911+ writeFile (t , filepath .Join (dir , "pom.xml" ), minimalPOM )
912+
913+ usage := map [string ]int {"does.not.exist" : 1 }
914+ results := resolveUnknownProperties (t .Context (), usage , nil , filepath .Join (dir , "pom.xml" ), dir )
915+
916+ if len (results ) != 0 {
917+ t .Errorf ("expected 0 results for unknown property, got %d" , len (results ))
918+ }
919+ }
0 commit comments