11using System ;
22using System . Collections . Generic ;
33using System . IO ;
4- using System . Text . RegularExpressions ;
4+ using System . Xml ;
55using Semmle . Util ;
66using Semmle . Util . Logging ;
77
88namespace Semmle . Extraction . CSharp . DependencyFetching
99{
1010
11- // <summary>
12- // This class is used to read a set of files and decide different properties about the
13- // content (by reading the content of the files only once).
14- // The implementation is lazy, so the properties are only calculated when
15- // the first property is accessed.
16- // </summary>
17- internal partial class FileContent
11+ /// <summary>
12+ /// Lazily reads XML files once to detect dependency and project settings without evaluating MSBuild.
13+ /// </summary>
14+ internal class FileContent
1815 {
1916 private readonly ILogger logger ;
2017 private readonly IUnsafeFileReader unsafeFileReader ;
@@ -135,46 +132,26 @@ internal FileContent(ILogger logger,
135132 public FileContent ( ILogger logger , IEnumerable < string > files ) : this ( logger , files , new UnsafeFileReader ( ) )
136133 { }
137134
138- private static string GetGroup ( ReadOnlySpan < char > input , ValueMatch valueMatch , string groupPrefix )
135+ private static string ? GetAttribute ( XmlReader reader , string name )
139136 {
140- var match = input . Slice ( valueMatch . Index , valueMatch . Length ) ;
141- var includeIndex = match . IndexOf ( groupPrefix , StringComparison . OrdinalIgnoreCase ) ;
142- if ( includeIndex == - 1 )
137+ while ( reader . MoveToNextAttribute ( ) )
143138 {
144- return string . Empty ;
145- }
146-
147- match = match . Slice ( includeIndex + groupPrefix . Length + 1 ) ;
148-
149- var quoteIndex1 = match . IndexOf ( "\" " ) ;
150- var quoteIndex2 = match . Slice ( quoteIndex1 + 1 ) . IndexOf ( "\" " ) ;
151-
152- return match . Slice ( quoteIndex1 + 1 , quoteIndex2 ) . ToString ( ) ;
153- }
154-
155- private static bool IsGroupMatch ( ReadOnlySpan < char > line , Regex regex , string groupPrefix , string value )
156- {
157- foreach ( var valueMatch in regex . EnumerateMatches ( line ) )
158- {
159- // We can't get the group from the ValueMatch, so doing it manually:
160- if ( string . Equals ( GetGroup ( line , valueMatch , groupPrefix ) , value , StringComparison . OrdinalIgnoreCase ) )
139+ if ( string . Equals ( reader . LocalName , name , StringComparison . OrdinalIgnoreCase ) )
161140 {
162- return true ;
141+ var value = reader . Value ;
142+ reader . MoveToElement ( ) ;
143+ return value ;
163144 }
164145 }
165- return false ;
146+ reader . MoveToElement ( ) ;
147+ return null ;
166148 }
167149
168- private void AddPackageReference ( ReadOnlySpan < char > line , string groupName , Func < Regex > regex , PackageReferenceSource source )
150+ private void AddPackageReference ( string ? packageName , PackageReferenceSource source )
169151 {
170- foreach ( var valueMatch in regex ( ) . EnumerateMatches ( line ) )
152+ if ( ! string . IsNullOrEmpty ( packageName ) )
171153 {
172- // We can't get the group from the ValueMatch, so doing it manually:
173- var packageName = GetGroup ( line , valueMatch , groupName ) . ToLowerInvariant ( ) ;
174- if ( ! string . IsNullOrEmpty ( packageName ) )
175- {
176- allPackages . Add ( new PackageReference ( packageName , source ) ) ;
177- }
154+ allPackages . Add ( new PackageReference ( packageName . ToLowerInvariant ( ) , source ) ) ;
178155 }
179156 }
180157
@@ -185,98 +162,85 @@ private void DoInitialize()
185162 try
186163 {
187164 var isPackagesConfig = string . Equals ( FileUtils . SafeGetFileName ( file , logger ) , "packages.config" , StringComparison . OrdinalIgnoreCase ) ;
165+ using var stream = unsafeFileReader . OpenRead ( file ) ;
166+ using var reader = XmlReader . Create ( stream , new XmlReaderSettings
167+ {
168+ DtdProcessing = DtdProcessing . Prohibit ,
169+ XmlResolver = null ,
170+ IgnoreComments = true ,
171+ IgnoreProcessingInstructions = true
172+ } ) ;
188173
189- foreach ( ReadOnlySpan < char > line in unsafeFileReader . ReadLines ( file ) )
174+ while ( ! reader . EOF )
190175 {
191- // Find all the packages.
192- if ( isPackagesConfig )
193- {
194- AddPackageReference ( line , "id" , LegacyPackageReference , PackageReferenceSource . PackagesConfig ) ;
195- }
196- else
176+ if ( reader . NodeType != XmlNodeType . Element )
197177 {
198- AddPackageReference ( line , "Include" , PackageReference , PackageReferenceSource . SdkCsProj ) ;
178+ reader . Read ( ) ;
179+ continue ;
199180 }
200181
201- // Determine if ASP.NET is used.
202- useAspNetCoreDlls = useAspNetCoreDlls
203- || IsGroupMatch ( line , ProjectSdk ( ) , "Sdk" , "Microsoft.NET.Sdk.Web" )
204- || IsGroupMatch ( line , FrameworkReference ( ) , "Include" , "Microsoft.AspNetCore.App" ) ;
205-
206- // Determine if implicit usings are used.
207- useImplicitUsings = useImplicitUsings
208- || line . Contains ( "<ImplicitUsings>enable</ImplicitUsings>" . AsSpan ( ) , StringComparison . OrdinalIgnoreCase )
209- || line . Contains ( "<ImplicitUsings>true</ImplicitUsings>" . AsSpan ( ) , StringComparison . OrdinalIgnoreCase ) ;
210-
211- // Determine if WPF is used.
212- useWpf = useWpf
213- || line . Contains ( "<UseWPF>true</UseWPF>" . AsSpan ( ) , StringComparison . OrdinalIgnoreCase ) ;
214-
215- // Determine if Windows Forms is used.
216- useWindowsForms = useWindowsForms
217- || line . Contains ( "<UseWindowsForms>true</UseWindowsForms>" . AsSpan ( ) , StringComparison . OrdinalIgnoreCase ) ;
218-
219- // Find all custom implicit usings.
220- foreach ( var valueMatch in CustomImplicitUsingDeclarations ( ) . EnumerateMatches ( line ) )
182+ switch ( reader . LocalName . ToLowerInvariant ( ) )
221183 {
222- var ns = GetGroup ( line , valueMatch , "Include" ) ;
223- if ( ! string . IsNullOrEmpty ( ns ) )
224- {
225- implicitUsingNamespaces . Add ( ns ) ;
226- }
184+ case "package" when isPackagesConfig :
185+ AddPackageReference ( GetAttribute ( reader , "id" ) , PackageReferenceSource . PackagesConfig ) ;
186+ break ;
187+ case "packagereference" when ! isPackagesConfig :
188+ AddPackageReference ( GetAttribute ( reader , "Include" ) , PackageReferenceSource . SdkCsProj ) ;
189+ break ;
190+ case "project" :
191+ var sdk = GetAttribute ( reader , "Sdk" ) ;
192+ isNewProjectStructureUsed |= sdk != null ;
193+ useAspNetCoreDlls |= string . Equals ( sdk , "Microsoft.NET.Sdk.Web" , StringComparison . OrdinalIgnoreCase ) ;
194+ break ;
195+ case "frameworkreference" :
196+ var framework = GetAttribute ( reader , "Include" ) ;
197+ isNewProjectStructureUsed |= framework != null ;
198+ useAspNetCoreDlls |= string . Equals ( framework , "Microsoft.AspNetCore.App" , StringComparison . OrdinalIgnoreCase ) ;
199+ break ;
200+ case "implicitusings" :
201+ var implicitUsings = reader . ReadElementContentAsString ( ) . Trim ( ) ;
202+ useImplicitUsings |= string . Equals ( implicitUsings , "enable" , StringComparison . OrdinalIgnoreCase )
203+ || string . Equals ( implicitUsings , "true" , StringComparison . OrdinalIgnoreCase ) ;
204+ continue ;
205+ case "usewpf" :
206+ useWpf |= string . Equals ( reader . ReadElementContentAsString ( ) . Trim ( ) , "true" , StringComparison . OrdinalIgnoreCase ) ;
207+ continue ;
208+ case "usewindowsforms" :
209+ useWindowsForms |= string . Equals ( reader . ReadElementContentAsString ( ) . Trim ( ) , "true" , StringComparison . OrdinalIgnoreCase ) ;
210+ continue ;
211+ case "using" :
212+ var ns = GetAttribute ( reader , "Include" ) ;
213+ if ( ! string . IsNullOrEmpty ( ns ) )
214+ {
215+ implicitUsingNamespaces . Add ( ns ) ;
216+ }
217+ break ;
218+ case "import" :
219+ isLegacyProjectStructureUsed |= GetAttribute ( reader , "Project" ) ? . EndsWith ( "Microsoft.CSharp.targets" , StringComparison . OrdinalIgnoreCase ) == true ;
220+ break ;
227221 }
228222
229- // Determine project structure:
230- isLegacyProjectStructureUsed = isLegacyProjectStructureUsed || MicrosoftCSharpTargets ( ) . IsMatch ( line ) ;
231- isNewProjectStructureUsed = isNewProjectStructureUsed
232- || ProjectSdk ( ) . IsMatch ( line )
233- || FrameworkReference ( ) . IsMatch ( line ) ;
234- // TODO: we could also check `<Sdk Name="Microsoft.NET.Sdk" />`
223+ reader . Read ( ) ;
235224 }
236225 }
237- catch ( Exception ex )
226+ catch ( Exception ex ) when ( ex is IOException or UnauthorizedAccessException or XmlException )
238227 {
239228 logger . LogInfo ( $ "Failed to read file { file } ") ;
240229 logger . LogDebug ( $ "Failed to read file { file } , exception: { ex } ") ;
241230 }
242231 }
243232 }
244233
245- [ GeneratedRegex ( "(?<!<!--.*)<PackageReference.*\\ sInclude=\" (.*?)\" .*/?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
246- private static partial Regex PackageReference ( ) ;
247-
248- [ GeneratedRegex ( "(?<!<!--.*)<package.*\\ sid=\" (.*?)\" .*/?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
249- private static partial Regex LegacyPackageReference ( ) ;
250-
251- [ GeneratedRegex ( "(?<!<!--.*)<FrameworkReference.*\\ sInclude=\" (.*?)\" .*/?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
252- private static partial Regex FrameworkReference ( ) ;
253-
254- [ GeneratedRegex ( "(?<!<!--.*)<(.*\\ s)?Project.*\\ sSdk=\" (.*?)\" .*/?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
255- private static partial Regex ProjectSdk ( ) ;
256-
257- [ GeneratedRegex ( "(?<!<!--.*)<Using.*\\ sInclude=\" (.*?)\" .*/?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
258- private static partial Regex CustomImplicitUsingDeclarations ( ) ;
259-
260- [ GeneratedRegex ( "(?<!<!--.*)<Import.*\\ sProject=\" .*Microsoft\\ .CSharp\\ .targets\" .*/?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
261- private static partial Regex MicrosoftCSharpTargets ( ) ;
262234 }
263235
264236 internal interface IUnsafeFileReader
265237 {
266- IEnumerable < string > ReadLines ( string file ) ;
238+ Stream OpenRead ( string file ) ;
267239 }
268240
269241 internal class UnsafeFileReader : IUnsafeFileReader
270242 {
271- public IEnumerable < string > ReadLines ( string file )
272- {
273- using var sr = new StreamReader ( file ) ;
274- string ? line ;
275- while ( ( line = sr . ReadLine ( ) ) != null )
276- {
277- yield return line ;
278- }
279- }
243+ public Stream OpenRead ( string file ) => File . OpenRead ( file ) ;
280244 }
281245
282246 public enum PackageReferenceSource
0 commit comments