Skip to content

Commit d391246

Browse files
committed
C#: Generate source files from .cshtml files in standalone
1 parent ba0f07b commit d391246

File tree

10 files changed

+356
-84
lines changed

10 files changed

+356
-84
lines changed

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

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ public sealed class DependencyManager : IDisposable
2323
private readonly IDictionary<string, string> unresolvedReferences = new ConcurrentDictionary<string, string>();
2424
private int failedProjects;
2525
private int succeededProjects;
26-
private readonly string[] allSources;
26+
private readonly List<string> allSources;
2727
private int conflictedReferences = 0;
2828
private readonly IDependencyOptions options;
2929
private readonly DirectoryInfo sourceDir;
3030
private readonly DotNet dotnet;
3131
private readonly FileContent fileContent;
3232
private readonly TemporaryDirectory packageDirectory;
33+
private readonly TemporaryDirectory? razorWorkingDirectory;
3334

3435

3536
/// <summary>
@@ -60,7 +61,7 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
6061
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
6162

6263
this.fileContent = new FileContent(packageDirectory, progressMonitor, () => GetFiles("*.*"));
63-
this.allSources = GetFiles("*.cs").ToArray();
64+
this.allSources = GetFiles("*.cs").ToList();
6465
var allProjects = GetFiles("*.csproj");
6566
var solutions = options.SolutionFile is not null
6667
? new[] { options.SolutionFile }
@@ -131,6 +132,32 @@ public DependencyManager(string srcDir, IDependencyOptions options, ILogger logg
131132
progressMonitor.UnresolvedReference(r.Key, r.Value);
132133
}
133134

135+
var views = GetFiles("*.cshtml")
136+
.Concat(GetFiles("*.razor"))
137+
.ToArray();
138+
139+
if (views.Length > 0)
140+
{
141+
// TODO: use SDK specified in global.json
142+
// TODO: add feature flag to control razor generation
143+
var sdk = new Sdk(dotnet).GetNewestSdk();
144+
if (sdk != null)
145+
{
146+
try
147+
{
148+
var razor = new Razor(sdk, dotnet, progressMonitor);
149+
razorWorkingDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "razor"));
150+
var generatedFiles = razor.GenerateFiles(views, usedReferences.Keys, razorWorkingDirectory.ToString());
151+
this.allSources.AddRange(generatedFiles);
152+
}
153+
catch (Exception ex)
154+
{
155+
// It's okay, we tried our best to generate source files from cshtml files.
156+
progressMonitor.LogInfo($"Failed to generate source files from cshtml files: {ex.Message}");
157+
}
158+
}
159+
}
160+
134161
progressMonitor.Summary(
135162
AllSourceFiles.Count(),
136163
ProjectSourceFiles.Count(),
@@ -156,17 +183,16 @@ private IEnumerable<string> GetFiles(string pattern, bool recurseSubdirectories
156183
/// Computes a unique temp directory for the packages associated
157184
/// with this source tree. Use a SHA1 of the directory name.
158185
/// </summary>
159-
/// <param name="srcDir"></param>
160186
/// <returns>The full path of the temp directory.</returns>
161-
private static string ComputeTempDirectory(string srcDir)
187+
private static string ComputeTempDirectory(string srcDir, string subfolderName = "packages")
162188
{
163189
var bytes = Encoding.Unicode.GetBytes(srcDir);
164190
var sha = SHA1.HashData(bytes);
165191
var sb = new StringBuilder();
166192
foreach (var b in sha.Take(8))
167193
sb.AppendFormat("{0:x2}", b);
168194

169-
return Path.Combine(Path.GetTempPath(), "GitHub", "packages", sb.ToString());
195+
return Path.Combine(Path.GetTempPath(), "GitHub", subfolderName, sb.ToString());
170196
}
171197

172198
/// <summary>
@@ -392,6 +418,10 @@ private void AnalyseSolutions(IEnumerable<string> solutions)
392418
});
393419
}
394420

395-
public void Dispose() => packageDirectory?.Dispose();
421+
public void Dispose()
422+
{
423+
packageDirectory?.Dispose();
424+
razorWorkingDirectory?.Dispose();
425+
}
396426
}
397427
}

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,6 @@
55

66
namespace Semmle.Extraction.CSharp.DependencyFetching
77
{
8-
internal interface IDotNet
9-
{
10-
bool RestoreToDirectory(string project, string directory, string? pathToNugetConfig = null);
11-
bool New(string folder);
12-
bool AddPackage(string folder, string package);
13-
IList<string> GetListedRuntimes();
14-
}
15-
168
/// <summary>
179
/// Utilities to run the "dotnet" command.
1810
/// </summary>
@@ -76,23 +68,33 @@ public bool AddPackage(string folder, string package)
7668
return RunCommand(args);
7769
}
7870

79-
public IList<string> GetListedRuntimes()
71+
public IList<string> GetListedRuntimes() => GetListed("--list-runtimes", "runtime");
72+
73+
public IList<string> GetListedSdks() => GetListed("--list-sdks", "SDK");
74+
75+
private IList<string> GetListed(string args, string artifact)
8076
{
81-
const string args = "--list-runtimes";
8277
progressMonitor.RunningProcess($"{dotnet} {args}");
8378
var pi = new ProcessStartInfo(dotnet, args)
8479
{
8580
RedirectStandardOutput = true,
8681
UseShellExecute = false
8782
};
88-
var exitCode = pi.ReadOutput(out var runtimes);
83+
var exitCode = pi.ReadOutput(out var artifacts);
8984
if (exitCode != 0)
9085
{
9186
progressMonitor.CommandFailed(dotnet, args, exitCode);
9287
return new List<string>();
9388
}
94-
progressMonitor.LogInfo($"Found runtimes: {string.Join("\n", runtimes)}");
95-
return runtimes;
89+
progressMonitor.LogInfo($"Found {artifact}s: {string.Join("\n", artifacts)}");
90+
return artifacts;
91+
}
92+
93+
public bool Exec(string execArgs)
94+
{
95+
// TODO: we might need to swallow the stdout of the started process to not pollute the logs of the extraction.
96+
var args = $"exec {execArgs}";
97+
return RunCommand(args);
9698
}
9799
}
98100
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,11 @@ public void MultipleNugetConfig(string[] nugetConfigs) =>
108108

109109
internal void NoTopLevelNugetConfig() =>
110110
LogInfo("Could not find a top-level nuget.config file.");
111+
112+
internal void RazorSourceGeneratorMissing(string fullPath) =>
113+
LogInfo($"Razor source generator folder {fullPath} does not exist.");
114+
115+
internal void CscMissing(string cscPath) =>
116+
LogInfo($"Csc.exe not found at {cscPath}.");
111117
}
112118
}

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

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ internal partial class Runtime
1717
private const string aspNetCoreApp = "Microsoft.AspNetCore.App";
1818

1919
private readonly IDotNet dotNet;
20-
private readonly Lazy<Dictionary<string, RuntimeVersion>> newestRuntimes;
21-
private Dictionary<string, RuntimeVersion> NewestRuntimes => newestRuntimes.Value;
20+
private readonly Lazy<Dictionary<string, DotnetVersion>> newestRuntimes;
21+
private Dictionary<string, DotnetVersion> NewestRuntimes => newestRuntimes.Value;
2222
private static string ExecutingRuntime => RuntimeEnvironment.GetRuntimeDirectory();
2323

2424
public Runtime(IDotNet dotNet)
@@ -27,58 +27,6 @@ public Runtime(IDotNet dotNet)
2727
this.newestRuntimes = new(GetNewestRuntimes);
2828
}
2929

30-
internal record RuntimeVersion : IComparable<RuntimeVersion>
31-
{
32-
private readonly string dir;
33-
private readonly Version version;
34-
private readonly Version? preReleaseVersion;
35-
private readonly string? preReleaseVersionType;
36-
private bool IsPreRelease => preReleaseVersionType is not null && preReleaseVersion is not null;
37-
public string FullPath
38-
{
39-
get
40-
{
41-
var preRelease = IsPreRelease ? $"-{preReleaseVersionType}.{preReleaseVersion}" : "";
42-
var version = this.version + preRelease;
43-
return Path.Combine(dir, version);
44-
}
45-
}
46-
47-
public RuntimeVersion(string dir, string version, string preReleaseVersionType, string preReleaseVersion)
48-
{
49-
this.dir = dir;
50-
this.version = Version.Parse(version);
51-
if (!string.IsNullOrEmpty(preReleaseVersion) && !string.IsNullOrEmpty(preReleaseVersionType))
52-
{
53-
this.preReleaseVersionType = preReleaseVersionType;
54-
this.preReleaseVersion = Version.Parse(preReleaseVersion);
55-
}
56-
}
57-
58-
public int CompareTo(RuntimeVersion? other)
59-
{
60-
var c = version.CompareTo(other?.version);
61-
if (c == 0 && IsPreRelease)
62-
{
63-
if (!other!.IsPreRelease)
64-
{
65-
return -1;
66-
}
67-
68-
// Both are pre-release like runtime versions.
69-
// The pre-release version types are sorted alphabetically (e.g. alpha, beta, preview, rc)
70-
// and the pre-release version types are more important that the pre-release version numbers.
71-
return preReleaseVersionType != other!.preReleaseVersionType
72-
? preReleaseVersionType!.CompareTo(other!.preReleaseVersionType)
73-
: preReleaseVersion!.CompareTo(other!.preReleaseVersion);
74-
}
75-
76-
return c;
77-
}
78-
79-
public override string ToString() => FullPath;
80-
}
81-
8230
[GeneratedRegex(@"^(\S+)\s(\d+\.\d+\.\d+)(-([a-z]+)\.(\d+\.\d+\.\d+))?\s\[(.+)\]$")]
8331
private static partial Regex RuntimeRegex();
8432

@@ -88,16 +36,17 @@ public int CompareTo(RuntimeVersion? other)
8836
/// It is assume that the format of a listed runtime is something like:
8937
/// Microsoft.NETCore.App 7.0.2 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
9038
/// </summary>
91-
private static Dictionary<string, RuntimeVersion> ParseRuntimes(IList<string> listed)
39+
private static Dictionary<string, DotnetVersion> ParseRuntimes(IList<string> listed)
9240
{
9341
// Parse listed runtimes.
94-
var runtimes = new Dictionary<string, RuntimeVersion>();
42+
var runtimes = new Dictionary<string, DotnetVersion>();
43+
var regex = RuntimeRegex();
9544
listed.ForEach(r =>
9645
{
97-
var match = RuntimeRegex().Match(r);
46+
var match = regex.Match(r);
9847
if (match.Success)
9948
{
100-
runtimes.AddOrUpdateToLatest(match.Groups[1].Value, new RuntimeVersion(match.Groups[6].Value, match.Groups[2].Value, match.Groups[4].Value, match.Groups[5].Value));
49+
runtimes.AddOrUpdateToLatest(match.Groups[1].Value, new DotnetVersion(match.Groups[6].Value, match.Groups[2].Value, match.Groups[4].Value, match.Groups[5].Value));
10150
}
10251
});
10352

@@ -107,7 +56,7 @@ private static Dictionary<string, RuntimeVersion> ParseRuntimes(IList<string> li
10756
/// <summary>
10857
/// Returns a dictionary mapping runtimes to their newest version.
10958
/// </summary>
110-
internal Dictionary<string, RuntimeVersion> GetNewestRuntimes()
59+
internal Dictionary<string, DotnetVersion> GetNewestRuntimes()
11160
{
11261
var listed = dotNet.GetListedRuntimes();
11362
return ParseRuntimes(listed);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace Semmle.Extraction.CSharp.Standalone
5+
{
6+
internal record DotnetVersion : IComparable<DotnetVersion>
7+
{
8+
private readonly string dir;
9+
private readonly Version version;
10+
private readonly Version? preReleaseVersion;
11+
private readonly string? preReleaseVersionType;
12+
private bool IsPreRelease => preReleaseVersionType is not null && preReleaseVersion is not null;
13+
public string FullPath
14+
{
15+
get
16+
{
17+
var preRelease = IsPreRelease ? $"-{preReleaseVersionType}.{preReleaseVersion}" : "";
18+
var version = this.version + preRelease;
19+
return Path.Combine(dir, version);
20+
}
21+
}
22+
23+
public DotnetVersion(string dir, string version, string preReleaseVersionType, string preReleaseVersion)
24+
{
25+
this.dir = dir;
26+
this.version = Version.Parse(version);
27+
if (!string.IsNullOrEmpty(preReleaseVersion) && !string.IsNullOrEmpty(preReleaseVersionType))
28+
{
29+
this.preReleaseVersionType = preReleaseVersionType;
30+
this.preReleaseVersion = Version.Parse(preReleaseVersion);
31+
}
32+
}
33+
34+
public int CompareTo(DotnetVersion? other)
35+
{
36+
var c = version.CompareTo(other?.version);
37+
if (c == 0 && IsPreRelease)
38+
{
39+
if (!other!.IsPreRelease)
40+
{
41+
return -1;
42+
}
43+
44+
// Both are pre-release like runtime versions.
45+
// The pre-release version types are sorted alphabetically (e.g. alpha, beta, preview, rc)
46+
// and the pre-release version types are more important that the pre-release version numbers.
47+
return preReleaseVersionType != other!.preReleaseVersionType
48+
? preReleaseVersionType!.CompareTo(other!.preReleaseVersionType)
49+
: preReleaseVersion!.CompareTo(other!.preReleaseVersion);
50+
}
51+
52+
return c;
53+
}
54+
55+
public override string ToString() => FullPath;
56+
}
57+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections.Generic;
2+
3+
namespace Semmle.BuildAnalyser
4+
{
5+
internal interface IDotNet
6+
{
7+
bool RestoreToDirectory(string project, string directory, string? pathToNugetConfig = null);
8+
bool New(string folder);
9+
bool AddPackage(string folder, string package);
10+
IList<string> GetListedRuntimes();
11+
IList<string> GetListedSdks();
12+
bool Exec(string execArgs);
13+
}
14+
}

0 commit comments

Comments
 (0)