11namespace Microsoft . ComponentDetection . Detectors . Go ;
22
3+ using System ;
4+ using System . Collections . Generic ;
35using System . IO ;
46using System . Text . RegularExpressions ;
57using System . Threading . Tasks ;
@@ -15,11 +17,50 @@ public class GoModParser : IGoParser
1517
1618 public GoModParser ( ILogger logger ) => this . logger = logger ;
1719
20+ /// <summary>
21+ /// Checks whether the input path is a potential local file system path
22+ /// 1. '.' checks whether the path is relative to current directory.
23+ /// 2. '..' checks whether the path is relative to some ancestor directory.
24+ /// 3. IsRootedPath checks whether it is an absolute path.
25+ /// </summary>
26+ /// <param name="path">Candidate path.</param>
27+ /// <returns>true if potential local file system path.</returns>
28+ private static bool IsLocalPath ( string path )
29+ {
30+ return path . StartsWith ( '.' ) || path . StartsWith ( ".." ) || Path . IsPathRooted ( path ) ;
31+ }
32+
33+ /// <summary>
34+ /// Tries to extract source token from replace directive.
35+ /// </summary>
36+ /// <param name="directiveLine">String containing a directive after replace token.</param>
37+ /// <param name="replaceDirectives">HashSet where the token is placed if replace directive substitutes a local path.</param>
38+ private static void TryExtractReplaceDirective ( string directiveLine , HashSet < string > replaceDirectives )
39+ {
40+ var parts = directiveLine . Split ( "=>" , StringSplitOptions . RemoveEmptyEntries ) ;
41+ if ( parts . Length == 2 )
42+ {
43+ var source = parts [ 0 ] . Trim ( ) . Split ( ' ' ) [ 0 ] ;
44+ var target = parts [ 1 ] . Trim ( ) ;
45+
46+ if ( IsLocalPath ( target ) )
47+ {
48+ replaceDirectives . Add ( source ) ;
49+ }
50+ }
51+ }
52+
1853 public async Task < bool > ParseAsync (
1954 ISingleFileComponentRecorder singleFileComponentRecorder ,
2055 IComponentStream file ,
2156 GoGraphTelemetryRecord record )
2257 {
58+ // Collect replace directives that point to a local path
59+ var replaceDirectives = await this . GetAllReplacePathDirectivesAsync ( file ) ;
60+
61+ // Rewind stream after reading replace directives
62+ file . Stream . Seek ( 0 , SeekOrigin . Begin ) ;
63+
2364 using var reader = new StreamReader ( file . Stream ) ;
2465
2566 // There can be multiple require( ) sections in go 1.17+. loop over all of them.
@@ -38,7 +79,7 @@ public async Task<bool> ParseAsync(
3879 // are listed in the require () section
3980 if ( line . StartsWith ( StartString ) )
4081 {
41- this . TryRegisterDependencyFromModLine ( line [ StartString . Length ..] , singleFileComponentRecorder ) ;
82+ this . TryRegisterDependencyFromModLine ( file , line [ StartString . Length ..] , singleFileComponentRecorder , replaceDirectives ) ;
4283 }
4384
4485 line = await reader . ReadLineAsync ( ) ;
@@ -47,14 +88,14 @@ public async Task<bool> ParseAsync(
4788 // Stopping at the first ) restrict the detection to only the require section.
4889 while ( ( line = await reader . ReadLineAsync ( ) ) != null && ! line . EndsWith ( ')' ) )
4990 {
50- this . TryRegisterDependencyFromModLine ( line , singleFileComponentRecorder ) ;
91+ this . TryRegisterDependencyFromModLine ( file , line , singleFileComponentRecorder , replaceDirectives ) ;
5192 }
5293 }
5394
5495 return true ;
5596 }
5697
57- private void TryRegisterDependencyFromModLine ( string line , ISingleFileComponentRecorder singleFileComponentRecorder )
98+ private void TryRegisterDependencyFromModLine ( IComponentStream file , string line , ISingleFileComponentRecorder singleFileComponentRecorder , HashSet < string > replaceDirectives )
5899 {
59100 if ( line . Trim ( ) . StartsWith ( "//" ) )
60101 {
@@ -64,6 +105,15 @@ private void TryRegisterDependencyFromModLine(string line, ISingleFileComponentR
64105
65106 if ( this . TryToCreateGoComponentFromModLine ( line , out var goComponent ) )
66107 {
108+ if ( replaceDirectives . Contains ( goComponent . Name ) )
109+ {
110+ // Skip registering this dependency since it's replaced by a local path
111+ // we will be reading this dependency somewhere else
112+ this . logger . LogInformation ( "Skipping {GoComponentId} from {Location} because it's a local reference." , goComponent . Id , file . Location ) ;
113+ return ;
114+ }
115+
116+ this . logger . LogError ( "Registering {GoComponent} from {Location}" , goComponent . Name , file . Location ) ;
67117 singleFileComponentRecorder . RegisterUsage ( new DetectedComponent ( goComponent ) ) ;
68118 }
69119 else
@@ -90,4 +140,47 @@ private bool TryToCreateGoComponentFromModLine(string line, out GoComponent goCo
90140
91141 return true ;
92142 }
143+
144+ private async Task < HashSet < string > > GetAllReplacePathDirectivesAsync ( IComponentStream file )
145+ {
146+ var replacedDirectives = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
147+ const string singleReplaceDirectiveBegin = "replace " ;
148+ const string multiReplaceDirectiveBegin = "replace (" ;
149+ using ( var reader = new StreamReader ( file . Stream , leaveOpen : true ) )
150+ {
151+ while ( ! reader . EndOfStream )
152+ {
153+ var line = await reader . ReadLineAsync ( ) ;
154+ if ( line == null )
155+ {
156+ continue ;
157+ }
158+
159+ line = line . Trim ( ) ;
160+
161+ // Multiline block: replace (
162+ if ( line . StartsWith ( multiReplaceDirectiveBegin ) )
163+ {
164+ while ( ( line = await reader . ReadLineAsync ( ) ) != null )
165+ {
166+ line = line . Trim ( ) ;
167+ if ( line == ")" )
168+ {
169+ break ;
170+ }
171+
172+ TryExtractReplaceDirective ( line , replacedDirectives ) ;
173+ }
174+ }
175+ else if ( line . StartsWith ( singleReplaceDirectiveBegin ) )
176+ {
177+ // single line block: replace
178+ var directiveContent = line [ singleReplaceDirectiveBegin . Length ..] . Trim ( ) ;
179+ TryExtractReplaceDirective ( directiveContent , replacedDirectives ) ;
180+ }
181+ }
182+ }
183+
184+ return replacedDirectives ;
185+ }
93186}
0 commit comments