Skip to content

Commit 15e26e1

Browse files
committed
C#: Reduce disabled nullability regions by splitting 'Extractor' and 'Analyser'
1 parent 2697677 commit 15e26e1

File tree

15 files changed

+678
-599
lines changed

15 files changed

+678
-599
lines changed

csharp/extractor/Semmle.Extraction.CIL/Analyser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Semmle.Extraction.CIL
77
{
88
public static class Analyser
99
{
10-
private static void ExtractCIL(Extractor extractor, TrapWriter trapWriter, bool extractPdbs)
10+
private static void ExtractCIL(NonStandaloneExtractor extractor, TrapWriter trapWriter, bool extractPdbs)
1111
{
1212
using var cilContext = new Context(extractor, trapWriter, extractor.OutputPath, extractPdbs);
1313
cilContext.Populate(new Assembly(cilContext));
@@ -33,7 +33,7 @@ public static void ExtractCIL(Layout layout, string assemblyPath, ILogger logger
3333
{
3434
var canonicalPathCache = CanonicalPathCache.Create(logger, 1000);
3535
var pathTransformer = new PathTransformer(canonicalPathCache);
36-
var extractor = new Extractor(assemblyPath, logger, pathTransformer);
36+
var extractor = new NonStandaloneExtractor(assemblyPath, logger, pathTransformer);
3737
var transformedAssemblyPath = pathTransformer.Transform(assemblyPath);
3838
var project = layout.LookupProjectOrDefault(transformedAssemblyPath);
3939
using var trapWriter = project.CreateTrapWriter(logger, transformedAssemblyPath.WithSuffix(".cil"), trapCompression, discardDuplicates: true);

csharp/extractor/Semmle.Extraction.CSharp/Analyser.cs

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

csharp/extractor/Semmle.Extraction.CSharp/Entities/Assembly.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ private Assembly(Context cx, Microsoft.CodeAnalysis.Location? init)
1818
if (init == null)
1919
{
2020
// This is the output assembly
21-
assemblyPath = cx.Extractor.OutputPath;
21+
assemblyPath = ((NonStandaloneExtractor)cx.Extractor).OutputPath;
2222
assembly = cx.Compilation.Assembly;
2323
}
2424
else
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
using System;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using System.IO;
5+
using System.Linq;
6+
using Semmle.Extraction.CSharp.Populators;
7+
using System.Collections.Generic;
8+
using System.Threading.Tasks;
9+
using System.Diagnostics;
10+
using Semmle.Util.Logging;
11+
12+
namespace Semmle.Extraction.CSharp
13+
{
14+
/// <summary>
15+
/// Encapsulates a C# analysis task.
16+
/// </summary>
17+
public class Analyser : IDisposable
18+
{
19+
protected Extraction.Extractor? extractor;
20+
protected CSharpCompilation? compilation;
21+
protected Layout? layout;
22+
protected CommonOptions? options;
23+
24+
private readonly object progressMutex = new object();
25+
26+
// The bulk of the extraction work, potentially executed in parallel.
27+
protected readonly List<Action> extractionTasks = new List<Action>();
28+
private int taskCount = 0;
29+
30+
private readonly Stopwatch stopWatch = new Stopwatch();
31+
32+
private readonly IProgressMonitor progressMonitor;
33+
34+
public ILogger Logger { get; }
35+
36+
protected readonly bool addAssemblyTrapPrefix;
37+
38+
public PathTransformer PathTransformer { get; }
39+
40+
protected Analyser(IProgressMonitor pm, ILogger logger, bool addAssemblyTrapPrefix, PathTransformer pathTransformer)
41+
{
42+
Logger = logger;
43+
this.addAssemblyTrapPrefix = addAssemblyTrapPrefix;
44+
Logger.Log(Severity.Info, "EXTRACTION STARTING at {0}", DateTime.Now);
45+
stopWatch.Start();
46+
progressMonitor = pm;
47+
PathTransformer = pathTransformer;
48+
}
49+
50+
/// <summary>
51+
/// Perform an analysis on a source file/syntax tree.
52+
/// </summary>
53+
/// <param name="tree">Syntax tree to analyse.</param>
54+
public void AnalyseTree(SyntaxTree tree)
55+
{
56+
extractionTasks.Add(() => DoExtractTree(tree));
57+
}
58+
59+
#nullable disable warnings
60+
61+
/// <summary>
62+
/// Enqueue all reference analysis tasks.
63+
/// </summary>
64+
public void AnalyseReferences()
65+
{
66+
foreach (var assembly in compilation.References.OfType<PortableExecutableReference>())
67+
{
68+
// CIL first - it takes longer.
69+
if (options.CIL)
70+
extractionTasks.Add(() => DoExtractCIL(assembly));
71+
extractionTasks.Add(() => DoAnalyseReferenceAssembly(assembly));
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Constructs the map from assembly string to its filename.
77+
///
78+
/// Roslyn doesn't record the relationship between a filename and its assembly
79+
/// information, so we need to retrieve this information manually.
80+
/// </summary>
81+
protected void SetReferencePaths()
82+
{
83+
foreach (var reference in compilation.References.OfType<PortableExecutableReference>())
84+
{
85+
try
86+
{
87+
var refPath = reference.FilePath!;
88+
89+
/* This method is significantly faster and more lightweight than using
90+
* System.Reflection.Assembly.ReflectionOnlyLoadFrom. It is also allows
91+
* loading the same assembly from different locations.
92+
*/
93+
using var pereader = new System.Reflection.PortableExecutable.PEReader(new FileStream(refPath, FileMode.Open, FileAccess.Read, FileShare.Read));
94+
95+
var metadata = pereader.GetMetadata();
96+
string assemblyIdentity;
97+
unsafe
98+
{
99+
var reader = new System.Reflection.Metadata.MetadataReader(metadata.Pointer, metadata.Length);
100+
var def = reader.GetAssemblyDefinition();
101+
assemblyIdentity = reader.GetString(def.Name) + " " + def.Version;
102+
}
103+
extractor.SetAssemblyFile(assemblyIdentity, refPath);
104+
105+
}
106+
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
107+
{
108+
extractor.Message(new Message("Exception reading reference file", reference.FilePath, null, ex.StackTrace));
109+
}
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Extract an assembly to a new trap file.
115+
/// If the trap file exists, skip extraction to avoid duplicating
116+
/// extraction within the snapshot.
117+
/// </summary>
118+
/// <param name="r">The assembly to extract.</param>
119+
private void DoAnalyseReferenceAssembly(PortableExecutableReference r)
120+
{
121+
try
122+
{
123+
var stopwatch = new Stopwatch();
124+
stopwatch.Start();
125+
126+
var assemblyPath = r.FilePath!;
127+
var transformedAssemblyPath = PathTransformer.Transform(assemblyPath);
128+
var projectLayout = layout.LookupProjectOrDefault(transformedAssemblyPath);
129+
using var trapWriter = projectLayout.CreateTrapWriter(Logger, transformedAssemblyPath, options.TrapCompression, discardDuplicates: true);
130+
131+
var skipExtraction = options.Cache && File.Exists(trapWriter.TrapFile);
132+
133+
if (!skipExtraction)
134+
{
135+
/* Note on parallel builds:
136+
*
137+
* The trap writer and source archiver both perform atomic moves
138+
* of the file to the final destination.
139+
*
140+
* If the same source file or trap file are generated concurrently
141+
* (by different parallel invocations of the extractor), then
142+
* last one wins.
143+
*
144+
* Specifically, if two assemblies are analysed concurrently in a build,
145+
* then there is a small amount of duplicated work but the output should
146+
* still be correct.
147+
*/
148+
149+
// compilation.Clone() reduces memory footprint by allowing the symbols
150+
// in c to be garbage collected.
151+
Compilation c = compilation.Clone();
152+
153+
154+
if (c.GetAssemblyOrModuleSymbol(r) is IAssemblySymbol assembly)
155+
{
156+
var cx = new Context(extractor, c, trapWriter, new AssemblyScope(assembly, assemblyPath), addAssemblyTrapPrefix);
157+
158+
foreach (var module in assembly.Modules)
159+
{
160+
AnalyseNamespace(cx, module.GlobalNamespace);
161+
}
162+
163+
Entities.Attribute.ExtractAttributes(cx, assembly, Entities.Assembly.Create(cx, assembly.GetSymbolLocation()));
164+
165+
cx.PopulateAll();
166+
}
167+
}
168+
169+
ReportProgress(assemblyPath, trapWriter.TrapFile, stopwatch.Elapsed, skipExtraction ? AnalysisAction.UpToDate : AnalysisAction.Extracted);
170+
}
171+
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
172+
{
173+
Logger.Log(Severity.Error, " Unhandled exception analyzing {0}: {1}", r.FilePath, ex);
174+
}
175+
}
176+
177+
private void DoExtractCIL(PortableExecutableReference r)
178+
{
179+
var stopwatch = new Stopwatch();
180+
stopwatch.Start();
181+
CIL.Analyser.ExtractCIL(layout, r.FilePath!, Logger, !options.Cache, options.PDB, options.TrapCompression, out var trapFile, out var extracted);
182+
stopwatch.Stop();
183+
ReportProgress(r.FilePath, trapFile, stopwatch.Elapsed, extracted ? AnalysisAction.Extracted : AnalysisAction.UpToDate);
184+
}
185+
186+
private void DoExtractTree(SyntaxTree tree)
187+
{
188+
try
189+
{
190+
var stopwatch = new Stopwatch();
191+
stopwatch.Start();
192+
var sourcePath = tree.FilePath;
193+
var transformedSourcePath = PathTransformer.Transform(sourcePath);
194+
195+
var projectLayout = layout.LookupProjectOrNull(transformedSourcePath);
196+
var excluded = projectLayout == null;
197+
var trapPath = excluded ? "" : projectLayout!.GetTrapPath(Logger, transformedSourcePath, options.TrapCompression);
198+
var upToDate = false;
199+
200+
if (!excluded)
201+
{
202+
// compilation.Clone() is used to allow symbols to be garbage collected.
203+
using var trapWriter = projectLayout!.CreateTrapWriter(Logger, transformedSourcePath, options.TrapCompression, discardDuplicates: false);
204+
205+
upToDate = options.Fast && FileIsUpToDate(sourcePath, trapWriter.TrapFile);
206+
207+
if (!upToDate)
208+
{
209+
var cx = new Context(extractor, compilation.Clone(), trapWriter, new SourceScope(tree), addAssemblyTrapPrefix);
210+
// Ensure that the file itself is populated in case the source file is totally empty
211+
var root = tree.GetRoot();
212+
Entities.File.Create(cx, root.SyntaxTree.FilePath);
213+
214+
var csNode = (CSharpSyntaxNode)root;
215+
csNode.Accept(new CompilationUnitVisitor(cx));
216+
csNode.Accept(new DirectiveVisitor(cx));
217+
cx.PopulateAll();
218+
CommentPopulator.ExtractCommentBlocks(cx, cx.CommentGenerator);
219+
cx.PopulateAll();
220+
}
221+
}
222+
223+
ReportProgress(sourcePath, trapPath, stopwatch.Elapsed, excluded
224+
? AnalysisAction.Excluded
225+
: upToDate
226+
? AnalysisAction.UpToDate
227+
: AnalysisAction.Extracted);
228+
}
229+
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
230+
{
231+
extractor.Message(new Message($"Unhandled exception processing syntax tree. {ex.Message}", tree.FilePath, null, ex.StackTrace));
232+
}
233+
}
234+
235+
#nullable restore warnings
236+
237+
private static bool FileIsUpToDate(string src, string dest)
238+
{
239+
return File.Exists(dest) &&
240+
File.GetLastWriteTime(dest) >= File.GetLastWriteTime(src);
241+
}
242+
243+
private void AnalyseNamespace(Context cx, INamespaceSymbol ns)
244+
{
245+
foreach (var memberNamespace in ns.GetNamespaceMembers())
246+
{
247+
AnalyseNamespace(cx, memberNamespace);
248+
}
249+
250+
foreach (var memberType in ns.GetTypeMembers())
251+
{
252+
Entities.Type.Create(cx, memberType).ExtractRecursive();
253+
}
254+
}
255+
256+
private void ReportProgress(string src, string output, TimeSpan time, AnalysisAction action)
257+
{
258+
lock (progressMutex)
259+
progressMonitor.Analysed(++taskCount, extractionTasks.Count, src, output, time, action);
260+
}
261+
262+
/// <summary>
263+
/// Run all extraction tasks.
264+
/// </summary>
265+
/// <param name="numberOfThreads">The number of threads to use.</param>
266+
public void PerformExtraction(int numberOfThreads)
267+
{
268+
Parallel.Invoke(
269+
new ParallelOptions { MaxDegreeOfParallelism = numberOfThreads },
270+
extractionTasks.ToArray());
271+
}
272+
273+
public virtual void Dispose()
274+
{
275+
stopWatch.Stop();
276+
Logger.Log(Severity.Info, " Peak working set = {0} MB", Process.GetCurrentProcess().PeakWorkingSet64 / (1024 * 1024));
277+
278+
if (TotalErrors > 0)
279+
Logger.Log(Severity.Info, "EXTRACTION FAILED with {0} error{1} in {2}", TotalErrors, TotalErrors == 1 ? "" : "s", stopWatch.Elapsed);
280+
else
281+
Logger.Log(Severity.Info, "EXTRACTION SUCCEEDED in {0}", stopWatch.Elapsed);
282+
283+
Logger.Dispose();
284+
}
285+
286+
/// <summary>
287+
/// Number of errors encountered during extraction.
288+
/// </summary>
289+
private int ExtractorErrors => extractor?.Errors ?? 0;
290+
291+
/// <summary>
292+
/// Number of errors encountered by the compiler.
293+
/// </summary>
294+
public int CompilationErrors { get; set; }
295+
296+
/// <summary>
297+
/// Total number of errors reported.
298+
/// </summary>
299+
public int TotalErrors => CompilationErrors + ExtractorErrors;
300+
301+
/// <summary>
302+
/// Logs information about the extractor.
303+
/// </summary>
304+
public void LogExtractorInfo(string extractorVersion)
305+
{
306+
Logger.Log(Severity.Info, " Extractor: {0}", Environment.GetCommandLineArgs().First());
307+
Logger.Log(Severity.Info, " Extractor version: {0}", extractorVersion);
308+
Logger.Log(Severity.Info, " Current working directory: {0}", Directory.GetCurrentDirectory());
309+
}
310+
}
311+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Semmle.Extraction.CSharp
2+
{
3+
/// <summary>
4+
/// What action was performed when extracting a file.
5+
/// </summary>
6+
public enum AnalysisAction
7+
{
8+
Extracted,
9+
UpToDate,
10+
Excluded
11+
}
12+
}

0 commit comments

Comments
 (0)