Skip to content

Commit 5537d79

Browse files
committed
Detect missing Xamarin SDKs
1 parent 7e48084 commit 5537d79

File tree

4 files changed

+210
-3
lines changed

4 files changed

+210
-3
lines changed

csharp/autobuilder/Semmle.Autobuild.CSharp/CSharpAutobuilder.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ public class CSharpAutobuilder : Autobuilder<CSharpAutobuildOptions>
3737

3838
private BuildCommandAutoRule? buildCommandAutoRule;
3939

40-
public CSharpAutobuilder(IBuildActions actions, CSharpAutobuildOptions options) : base(actions, options) { }
40+
private readonly DiagnosticClassifier diagnosticClassifier;
41+
42+
protected override DiagnosticClassifier DiagnosticClassifier => diagnosticClassifier;
43+
44+
public CSharpAutobuilder(IBuildActions actions, CSharpAutobuildOptions options) : base(actions, options)
45+
{
46+
diagnosticClassifier = new CSharpDiagnosticClassifier();
47+
}
4148

4249
public override BuildScript GetBuildScript()
4350
{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Linq;
3+
using System.Text.RegularExpressions;
4+
using Semmle.Autobuild.Shared;
5+
using Semmle.Util;
6+
7+
namespace Semmle.Autobuild.CSharp
8+
{
9+
/// <summary>
10+
/// A diagnostic rule which tries to identify missing Xamarin SDKs.
11+
/// </summary>
12+
public class MissingXamarinSdkRule : DiagnosticRule
13+
{
14+
public class Result : IDiagnosticsResult
15+
{
16+
/// <summary>
17+
/// The name of the SDK that is missing.
18+
/// </summary>
19+
public string SDKName { get; }
20+
21+
public Result(string sdkName)
22+
{
23+
this.SDKName = sdkName;
24+
}
25+
26+
public DiagnosticMessage ToDiagnosticMessage<T>(Autobuilder<T> builder) where T : AutobuildOptionsShared
27+
{
28+
var diag = builder.MakeDiagnostic(
29+
$"missing-xamarin-{this.SDKName.ToLower()}-sdk",
30+
$"Missing Xamarin SDK for {this.SDKName}"
31+
);
32+
diag.PlaintextMessage = "Please install this SDK before running CodeQL.";
33+
34+
return diag;
35+
}
36+
}
37+
38+
public MissingXamarinSdkRule() :
39+
base("MSB4019:[^\"]*\"[^\"]*/Xamarin/(?<sdkName>[^/]*)/Xamarin\\.(\\k<sdkName>)\\.CSharp\\.targets\"")
40+
{
41+
}
42+
43+
public override void Fire(DiagnosticClassifier classifier, Match match)
44+
{
45+
if (!match.Groups.ContainsKey("sdkName"))
46+
throw new ArgumentException("Expected regular expression match to contain sdkName");
47+
48+
var sdkName = match.Groups["sdkName"].Value;
49+
var xamarinResults = classifier.Results.OfType<Result>().Where(result =>
50+
result.SDKName.Equals(sdkName)
51+
);
52+
53+
if (!xamarinResults.Any())
54+
classifier.Results.Add(new Result(sdkName));
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Implements a <see cref="DiagnosticClassifier" /> which applies C#-specific rules to
60+
/// the build output.
61+
/// </summary>
62+
public class CSharpDiagnosticClassifier : DiagnosticClassifier
63+
{
64+
public CSharpDiagnosticClassifier()
65+
{
66+
// add C#-specific rules to this classifier
67+
this.AddRule(new MissingXamarinSdkRule());
68+
}
69+
}
70+
}

csharp/autobuilder/Semmle.Autobuild.Shared/Autobuilder.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ protected string RequireEnvironmentVariable(string name)
264264

265265
protected string DiagnosticsDir { get; }
266266

267+
protected abstract DiagnosticClassifier DiagnosticClassifier { get; }
268+
267269
private readonly ILogger logger = new ConsoleLogger(Verbosity.Info);
268270

269271
private readonly DiagnosticsStream diagnostics;
@@ -310,7 +312,23 @@ void exitCallback(int ret, string msg, bool silent)
310312
Log(silent ? Severity.Debug : Severity.Info, $"Exit code {ret}{(string.IsNullOrEmpty(msg) ? "" : $": {msg}")}");
311313
}
312314

313-
return script.Run(Actions, startCallback, exitCallback);
315+
var onOutput = BuildOutputHandler(Console.Out);
316+
var onError = BuildOutputHandler(Console.Error);
317+
318+
var buildResult = script.Run(Actions, startCallback, exitCallback, onOutput, onError);
319+
320+
// if the build succeeded, all diagnostics we captured from the build output should be warnings;
321+
// otherwise they should all be errors
322+
var diagSeverity = buildResult == 0 ? DiagnosticMessage.TspSeverity.Warning : DiagnosticMessage.TspSeverity.Error;
323+
foreach (var diagResult in this.DiagnosticClassifier.Results)
324+
{
325+
var diag = diagResult.ToDiagnosticMessage(this);
326+
diag.Severity = diagSeverity;
327+
328+
Diagnostic(diag);
329+
}
330+
331+
return buildResult;
314332
}
315333

316334
/// <summary>
@@ -325,7 +343,7 @@ void exitCallback(int ret, string msg, bool silent)
325343
/// <param name="id">The last part of the message id.</param>
326344
/// <param name="name">The human-friendly description of the message.</param>
327345
/// <returns>The resulting <see cref="DiagnosticMessage" />.</returns>
328-
protected DiagnosticMessage MakeDiagnostic(string id, string name)
346+
public DiagnosticMessage MakeDiagnostic(string id, string name)
329347
{
330348
DiagnosticMessage diag = new(new($"{this.Options.Language.UpperCaseName.ToLower()}/autobuilder/{id}", name));
331349
diag.Source.ExtractorName = Options.Language.UpperCaseName.ToLower();
@@ -368,6 +386,24 @@ protected BuildScript AutobuildFailure() =>
368386
return 1;
369387
});
370388

389+
/// <summary>
390+
/// Constructs a <see cref="BuildOutputHandler" /> which uses the <see cref="DiagnosticClassifier" />
391+
/// to classify build output. All data also gets written to <paramref name="writer" />.
392+
/// </summary>
393+
/// <param name="writer">
394+
/// The <see cref="TextWriter" /> to which the build output would have normally been written to.
395+
/// This is normally <see cref="Console.Out" /> or <see cref="Console.Error" />.
396+
/// </param>
397+
/// <returns>The constructed <see cref="BuildOutputHandler" />.</returns>
398+
protected BuildOutputHandler BuildOutputHandler(TextWriter writer) => new(data =>
399+
{
400+
if (data is not null)
401+
{
402+
writer.WriteLine(data);
403+
DiagnosticClassifier.ClassifyLine(data);
404+
}
405+
});
406+
371407
/// <summary>
372408
/// Value of CODEQL_EXTRACTOR_<LANG>_ROOT environment variable.
373409
/// </summary>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Collections.Generic;
2+
using System.Text.RegularExpressions;
3+
using Semmle.Util;
4+
5+
namespace Semmle.Autobuild.Shared
6+
{
7+
/// <summary>
8+
/// Direct results result from the successful application of a <see cref="DiagnosticRule" />,
9+
/// which can later be converted to a corresponding <see cref="DiagnosticMessage" />.
10+
/// </summary>
11+
public interface IDiagnosticsResult
12+
{
13+
/// <summary>
14+
/// Produces a <see cref="DiagnosticMessage" /> corresponding to this result.
15+
/// </summary>
16+
/// <param name="builder">
17+
/// The autobuilder to use for constructing the base <see cref="DiagnosticMessage" />.
18+
/// </param>
19+
/// <returns>The <see cref="DiagnosticMessage" /> corresponding to this result.</returns>
20+
DiagnosticMessage ToDiagnosticMessage<T>(Autobuilder<T> builder) where T : AutobuildOptionsShared;
21+
}
22+
23+
public class DiagnosticRule
24+
{
25+
/// <summary>
26+
/// The pattern against which this rule matches build output.
27+
/// </summary>
28+
public Regex Pattern { get; }
29+
30+
/// <summary>
31+
/// Constructs a diagnostic rule for the given <paramref name="pattern" />.
32+
/// </summary>
33+
/// <param name="pattern"></param>
34+
public DiagnosticRule(Regex pattern)
35+
{
36+
this.Pattern = pattern;
37+
}
38+
39+
/// <summary>
40+
/// Constructs a diagnostic rule for the given regular expression <paramref name="pattern" />.
41+
/// </summary>
42+
/// <param name="pattern"></param>
43+
public DiagnosticRule(string pattern)
44+
{
45+
this.Pattern = new Regex(pattern, RegexOptions.Compiled);
46+
}
47+
48+
/// <summary>
49+
/// Used by a <see cref="DiagnosticClassifier" /> <paramref name="classifier" /> to
50+
/// signal that the rule has matched some build output with <paramref name="match" />.
51+
/// </summary>
52+
/// <param name="classifier">The classifier which is firing the rule.</param>
53+
/// <param name="match">The <see cref="Match" /> that resulted from applying the rule.</param>
54+
public virtual void Fire(DiagnosticClassifier classifier, Match match) { }
55+
}
56+
57+
public class DiagnosticClassifier
58+
{
59+
private readonly List<DiagnosticRule> rules;
60+
public readonly List<IDiagnosticsResult> Results;
61+
62+
public DiagnosticClassifier()
63+
{
64+
this.rules = new List<DiagnosticRule>();
65+
this.Results = new List<IDiagnosticsResult>();
66+
}
67+
68+
/// <summary>
69+
/// Adds <paramref name="rule" /> to this classifier.
70+
/// </summary>
71+
/// <param name="rule">The rule to add.</param>
72+
protected void AddRule(DiagnosticRule rule)
73+
{
74+
this.rules.Add(rule);
75+
}
76+
77+
/// <summary>
78+
/// Applies all of this classifier's rules to <paramref name="line" /> to see which match.
79+
/// </summary>
80+
/// <param name="line">The line to which the rules should be applied to.</param>
81+
public void ClassifyLine(string line)
82+
{
83+
foreach (var rule in this.rules)
84+
{
85+
var match = rule.Pattern.Match(line);
86+
if (match.Success)
87+
{
88+
rule.Fire(this, match);
89+
}
90+
}
91+
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)