Skip to content

Commit c4d7302

Browse files
authored
Merge pull request github#14228 from tamasvajk/standalone-implicit-usings
C#: Generate source file with implicit usings in Standalone
2 parents bd31e10 + fa814a5 commit c4d7302

File tree

8 files changed

+222
-95
lines changed

8 files changed

+222
-95
lines changed

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ public sealed class DependencyManager : IDisposable
3030
private readonly DotNet dotnet;
3131
private readonly FileContent fileContent;
3232
private readonly TemporaryDirectory packageDirectory;
33-
private TemporaryDirectory? razorWorkingDirectory;
34-
private readonly Git git;
35-
33+
private readonly TemporaryDirectory tempWorkingDirectory;
3634

3735
/// <summary>
3836
/// Performs C# dependency fetching.
@@ -60,20 +58,21 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
6058
this.progressMonitor.FindingFiles(srcDir);
6159

6260
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
63-
var allFiles = GetAllFiles().ToList();
64-
var smallFiles = allFiles.SelectSmallFiles(progressMonitor).SelectFileNames();
65-
this.fileContent = new FileContent(progressMonitor, smallFiles);
66-
this.allSources = allFiles.SelectFileNamesByExtension(".cs").ToList();
67-
var allProjects = allFiles.SelectFileNamesByExtension(".csproj");
61+
tempWorkingDirectory = new TemporaryDirectory(GetTemporaryWorkingDirectory());
62+
63+
var allFiles = GetAllFiles();
64+
var binaryFileExtensions = new HashSet<string>(new[] { ".dll", ".exe" }); // TODO: add more binary file extensions.
65+
var allNonBinaryFiles = allFiles.Where(f => !binaryFileExtensions.Contains(f.Extension.ToLowerInvariant())).ToList();
66+
var smallNonBinaryFiles = allNonBinaryFiles.SelectSmallFiles(progressMonitor).SelectFileNames();
67+
this.fileContent = new FileContent(progressMonitor, smallNonBinaryFiles);
68+
this.allSources = allNonBinaryFiles.SelectFileNamesByExtension(".cs").ToList();
69+
var allProjects = allNonBinaryFiles.SelectFileNamesByExtension(".csproj");
6870
var solutions = options.SolutionFile is not null
6971
? new[] { options.SolutionFile }
70-
: allFiles.SelectFileNamesByExtension(".sln");
71-
72-
// If DLL reference paths are specified on the command-line, use those to discover
73-
// assemblies. Otherwise (the default), query the git CLI to determine which DLL files
74-
// are tracked as part of the repository.
75-
this.git = new Git(this.progressMonitor);
76-
var dllDirNames = options.DllDirs.Count == 0 ? this.git.ListFiles("*.dll") : options.DllDirs.Select(Path.GetFullPath).ToList();
72+
: allNonBinaryFiles.SelectFileNamesByExtension(".sln");
73+
var dllDirNames = options.DllDirs.Count == 0
74+
? allFiles.SelectFileNamesByExtension(".dll").ToList()
75+
: options.DllDirs.Select(Path.GetFullPath).ToList();
7776

7877
// Find DLLs in the .Net / Asp.Net Framework
7978
if (options.ScanNetFrameworkDlls)
@@ -106,7 +105,7 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
106105
var restoredProjects = RestoreSolutions(solutions);
107106
var projects = allProjects.Except(restoredProjects);
108107
RestoreProjects(projects);
109-
DownloadMissingPackages(allFiles);
108+
DownloadMissingPackages(allNonBinaryFiles);
110109
}
111110

112111
assemblyCache = new AssemblyCache(dllDirNames, progressMonitor);
@@ -134,9 +133,11 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
134133
if (bool.TryParse(webViewExtractionOption, out var shouldExtractWebViews) &&
135134
shouldExtractWebViews)
136135
{
137-
GenerateSourceFilesFromWebViews(allFiles);
136+
GenerateSourceFilesFromWebViews(allNonBinaryFiles);
138137
}
139138

139+
GenerateSourceFileFromImplicitUsings();
140+
140141
progressMonitor.Summary(
141142
AllSourceFiles.Count(),
142143
ProjectSourceFiles.Count(),
@@ -149,6 +150,46 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
149150
DateTime.Now - startTime);
150151
}
151152

153+
private void GenerateSourceFileFromImplicitUsings()
154+
{
155+
var usings = new HashSet<string>();
156+
if (!fileContent.UseImplicitUsings)
157+
{
158+
return;
159+
}
160+
161+
// Hardcoded values from https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview#implicit-using-directives
162+
usings.UnionWith(new[] { "System", "System.Collections.Generic", "System.IO", "System.Linq", "System.Net.Http", "System.Threading",
163+
"System.Threading.Tasks" });
164+
165+
if (fileContent.UseAspNetDlls)
166+
{
167+
usings.UnionWith(new[] { "System.Net.Http.Json", "Microsoft.AspNetCore.Builder", "Microsoft.AspNetCore.Hosting",
168+
"Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Routing", "Microsoft.Extensions.Configuration",
169+
"Microsoft.Extensions.DependencyInjection", "Microsoft.Extensions.Hosting", "Microsoft.Extensions.Logging" });
170+
}
171+
172+
usings.UnionWith(fileContent.CustomImplicitUsings);
173+
174+
if (usings.Count > 0)
175+
{
176+
var tempDir = GetTemporaryWorkingDirectory("implicitUsings");
177+
var path = Path.Combine(tempDir, "GlobalUsings.g.cs");
178+
using (var writer = new StreamWriter(path))
179+
{
180+
writer.WriteLine("// <auto-generated/>");
181+
writer.WriteLine("");
182+
183+
foreach (var u in usings.OrderBy(u => u))
184+
{
185+
writer.WriteLine($"global using global::{u};");
186+
}
187+
}
188+
189+
this.allSources.Add(path);
190+
}
191+
}
192+
152193
private void GenerateSourceFilesFromWebViews(List<FileInfo> allFiles)
153194
{
154195
progressMonitor.LogInfo($"Generating source files from cshtml and razor files.");
@@ -165,8 +206,8 @@ private void GenerateSourceFilesFromWebViews(List<FileInfo> allFiles)
165206
try
166207
{
167208
var razor = new Razor(sdk, dotnet, progressMonitor);
168-
razorWorkingDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "razor"));
169-
var generatedFiles = razor.GenerateFiles(views, usedReferences.Keys, razorWorkingDirectory.ToString());
209+
var targetDir = GetTemporaryWorkingDirectory("razor");
210+
var generatedFiles = razor.GenerateFiles(views, usedReferences.Keys, targetDir);
170211
this.allSources.AddRange(generatedFiles);
171212
}
172213
catch (Exception ex)
@@ -180,24 +221,61 @@ private void GenerateSourceFilesFromWebViews(List<FileInfo> allFiles)
180221

181222
public DependencyManager(string srcDir) : this(srcDir, DependencyOptions.Default, new ConsoleLogger(Verbosity.Info)) { }
182223

183-
private IEnumerable<FileInfo> GetAllFiles() =>
184-
sourceDir.GetFiles("*.*", new EnumerationOptions { RecurseSubdirectories = true })
185-
.Where(d => d.Extension != ".dll" && !options.ExcludesFile(d.FullName));
224+
private IEnumerable<FileInfo> GetAllFiles()
225+
{
226+
var files = sourceDir.GetFiles("*.*", new EnumerationOptions { RecurseSubdirectories = true })
227+
.Where(d => !options.ExcludesFile(d.FullName));
228+
229+
if (options.DotNetPath != null)
230+
{
231+
files = files.Where(f => !f.FullName.StartsWith(options.DotNetPath, StringComparison.OrdinalIgnoreCase));
232+
}
233+
234+
return files;
235+
}
186236

187237
/// <summary>
188238
/// Computes a unique temp directory for the packages associated
189239
/// with this source tree. Use a SHA1 of the directory name.
190240
/// </summary>
191241
/// <returns>The full path of the temp directory.</returns>
192-
private static string ComputeTempDirectory(string srcDir, string subfolderName = "packages")
242+
private static string ComputeTempDirectory(string srcDir)
193243
{
194244
var bytes = Encoding.Unicode.GetBytes(srcDir);
195245
var sha = SHA1.HashData(bytes);
196246
var sb = new StringBuilder();
197247
foreach (var b in sha.Take(8))
198248
sb.AppendFormat("{0:x2}", b);
199249

200-
return Path.Combine(Path.GetTempPath(), "GitHub", subfolderName, sb.ToString());
250+
return Path.Combine(Path.GetTempPath(), "GitHub", "packages", sb.ToString());
251+
}
252+
253+
private static string GetTemporaryWorkingDirectory()
254+
{
255+
var tempFolder = EnvironmentVariables.GetScratchDirectory();
256+
257+
if (string.IsNullOrEmpty(tempFolder))
258+
{
259+
var tempPath = Path.GetTempPath();
260+
var name = Guid.NewGuid().ToString("N").ToUpper();
261+
tempFolder = Path.Combine(tempPath, "GitHub", name);
262+
}
263+
264+
return tempFolder;
265+
}
266+
267+
/// <summary>
268+
/// Creates a temporary directory with the given subfolder name.
269+
/// The created directory might be inside the repo folder, and it is deleted when the object is disposed.
270+
/// </summary>
271+
/// <param name="subfolder"></param>
272+
/// <returns></returns>
273+
private string GetTemporaryWorkingDirectory(string subfolder)
274+
{
275+
var temp = Path.Combine(tempWorkingDirectory.ToString(), subfolder);
276+
Directory.CreateDirectory(temp);
277+
278+
return temp;
201279
}
202280

203281
/// <summary>
@@ -424,7 +502,7 @@ private void DownloadMissingPackages(List<FileInfo> allFiles)
424502
foreach (var package in notYetDownloadedPackages)
425503
{
426504
progressMonitor.NugetInstall(package);
427-
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package));
505+
using var tempDir = new TemporaryDirectory(GetTemporaryWorkingDirectory(package));
428506
var success = dotnet.New(tempDir.DirInfo.FullName);
429507
if (!success)
430508
{
@@ -467,7 +545,7 @@ private void AnalyseSolutions(IEnumerable<string> solutions)
467545
public void Dispose()
468546
{
469547
packageDirectory?.Dispose();
470-
razorWorkingDirectory?.Dispose();
548+
tempWorkingDirectory?.Dispose();
471549
}
472550
}
473551
}

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FileContent.cs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal partial class FileContent
1919
private readonly IUnsafeFileReader unsafeFileReader;
2020
private readonly IEnumerable<string> files;
2121
private readonly HashSet<string> allPackages = new HashSet<string>();
22+
private readonly HashSet<string> implicitUsingNamespaces = new HashSet<string>();
2223
private readonly Initializer initialize;
2324

2425
public HashSet<string> AllPackages
@@ -48,6 +49,26 @@ public bool UseAspNetDlls
4849
}
4950
}
5051

52+
private bool useImplicitUsings = false;
53+
54+
public bool UseImplicitUsings
55+
{
56+
get
57+
{
58+
initialize.Run();
59+
return useImplicitUsings;
60+
}
61+
}
62+
63+
public HashSet<string> CustomImplicitUsings
64+
{
65+
get
66+
{
67+
initialize.Run();
68+
return implicitUsingNamespaces;
69+
}
70+
}
71+
5172
internal FileContent(ProgressMonitor progressMonitor,
5273
IEnumerable<string> files,
5374
IUnsafeFileReader unsafeFileReader)
@@ -62,7 +83,7 @@ internal FileContent(ProgressMonitor progressMonitor,
6283
public FileContent(ProgressMonitor progressMonitor, IEnumerable<string> files) : this(progressMonitor, files, new UnsafeFileReader())
6384
{ }
6485

65-
private static string GetGroup(ReadOnlySpan<char> input, ValueMatch valueMatch, string groupPrefix)
86+
private static string GetGroup(ReadOnlySpan<char> input, ValueMatch valueMatch, string groupPrefix, bool toLower)
6687
{
6788
var match = input.Slice(valueMatch.Index, valueMatch.Length);
6889
var includeIndex = match.IndexOf(groupPrefix, StringComparison.InvariantCultureIgnoreCase);
@@ -76,15 +97,22 @@ private static string GetGroup(ReadOnlySpan<char> input, ValueMatch valueMatch,
7697
var quoteIndex1 = match.IndexOf("\"");
7798
var quoteIndex2 = match.Slice(quoteIndex1 + 1).IndexOf("\"");
7899

79-
return match.Slice(quoteIndex1 + 1, quoteIndex2).ToString().ToLowerInvariant();
100+
var result = match.Slice(quoteIndex1 + 1, quoteIndex2).ToString();
101+
102+
if (toLower)
103+
{
104+
result = result.ToLowerInvariant();
105+
}
106+
107+
return result;
80108
}
81109

82110
private static bool IsGroupMatch(ReadOnlySpan<char> line, Regex regex, string groupPrefix, string value)
83111
{
84112
foreach (var valueMatch in regex.EnumerateMatches(line))
85113
{
86114
// We can't get the group from the ValueMatch, so doing it manually:
87-
if (GetGroup(line, valueMatch, groupPrefix) == value.ToLowerInvariant())
115+
if (GetGroup(line, valueMatch, groupPrefix, toLower: true) == value.ToLowerInvariant())
88116
{
89117
return true;
90118
}
@@ -105,7 +133,7 @@ private void DoInitialize()
105133
foreach (var valueMatch in PackageReference().EnumerateMatches(line))
106134
{
107135
// We can't get the group from the ValueMatch, so doing it manually:
108-
var packageName = GetGroup(line, valueMatch, "Include");
136+
var packageName = GetGroup(line, valueMatch, "Include", toLower: true);
109137
if (!string.IsNullOrEmpty(packageName))
110138
{
111139
allPackages.Add(packageName);
@@ -119,6 +147,23 @@ private void DoInitialize()
119147
IsGroupMatch(line, ProjectSdk(), "Sdk", "Microsoft.NET.Sdk.Web") ||
120148
IsGroupMatch(line, FrameworkReference(), "Include", "Microsoft.AspNetCore.App");
121149
}
150+
151+
// Determine if implicit usings are used.
152+
if (!useImplicitUsings)
153+
{
154+
useImplicitUsings = line.Contains("<ImplicitUsings>enable</ImplicitUsings>".AsSpan(), StringComparison.Ordinal) ||
155+
line.Contains("<ImplicitUsings>true</ImplicitUsings>".AsSpan(), StringComparison.Ordinal);
156+
}
157+
158+
// Find all custom implicit usings.
159+
foreach (var valueMatch in CustomImplicitUsingDeclarations().EnumerateMatches(line))
160+
{
161+
var ns = GetGroup(line, valueMatch, "Include", toLower: false);
162+
if (!string.IsNullOrEmpty(ns))
163+
{
164+
implicitUsingNamespaces.Add(ns);
165+
}
166+
}
122167
}
123168
}
124169
catch (Exception ex)
@@ -136,6 +181,9 @@ private void DoInitialize()
136181

137182
[GeneratedRegex("<(.*\\s)?Project.*\\sSdk=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
138183
private static partial Regex ProjectSdk();
184+
185+
[GeneratedRegex("<Using.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
186+
private static partial Regex CustomImplicitUsingDeclarations();
139187
}
140188

141189
internal interface IUnsafeFileReader

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/Git.cs

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)