Skip to content

Commit 9112135

Browse files
authored
Combine multiple suppressions applied to the same diagnostic (#1699)
* Ensure multiply suppressed diagnostics only create one output * Add extra tests * Use other API to get suppression attribute types
1 parent 00e0884 commit 9112135

File tree

4 files changed

+254
-120
lines changed

4 files changed

+254
-120
lines changed

Engine/Generic/RuleSuppression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ public static List<RuleSuppression> GetSuppressions(IEnumerable<AttributeAst> at
308308
}
309309

310310
IEnumerable<AttributeAst> suppressionAttribute = attrAsts.Where(
311-
item => item.TypeName.GetReflectionType() == typeof(System.Diagnostics.CodeAnalysis.SuppressMessageAttribute));
311+
item => item.TypeName.GetReflectionAttributeType() == typeof(System.Diagnostics.CodeAnalysis.SuppressMessageAttribute));
312312

313313
foreach (var attributeAst in suppressionAttribute)
314314
{

Engine/Generic/SuppressedRecord.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
6+
47
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
58
{
69
/// <summary>
@@ -9,22 +12,18 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
912
public class SuppressedRecord : DiagnosticRecord
1013
{
1114
/// <summary>
12-
/// The rule suppression of this record
15+
/// The rule suppressions applied to this record.
1316
/// </summary>
14-
public RuleSuppression Suppression
15-
{
16-
get;
17-
set;
18-
}
17+
public IReadOnlyList<RuleSuppression> Suppression { get; set; }
1918

2019
/// <summary>
2120
/// Creates a suppressed record based on a diagnostic record and the rule suppression
2221
/// </summary>
2322
/// <param name="record"></param>
2423
/// <param name="Suppression"></param>
25-
public SuppressedRecord(DiagnosticRecord record, RuleSuppression suppression)
24+
public SuppressedRecord(DiagnosticRecord record, IReadOnlyList<RuleSuppression> suppressions)
2625
{
27-
Suppression = suppression;
26+
Suppression = new ReadOnlyCollection<RuleSuppression>(new List<RuleSuppression>(suppressions));
2827
if (record != null)
2928
{
3029
RuleName = record.RuleName;

Engine/Helper.cs

Lines changed: 131 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,156 +1323,177 @@ internal List<RuleSuppression> GetSuppressionsConfiguration(ConfigurationDefinit
13231323
/// Suppress the rules from the diagnostic records list.
13241324
/// Returns a list of suppressed records as well as the ones that are not suppressed
13251325
/// </summary>
1326-
/// <param name="ruleSuppressions"></param>
1327-
/// <param name="diagnostics"></param>
1326+
/// <param name="ruleName">The name of the rule possibly being suppressed.</param>
1327+
/// <param name="ruleSuppressionsDict">A lookup table from rule name to suppressions of that rule.</param>
1328+
/// <param name="diagnostics">The list of all diagnostics emitted by the given rule.</param>
1329+
/// <param name="errors">Any errors that arise due to misconfigured suppression settings.</param>
1330+
/// <returns>A pair, with the first item being the list of suppressed diagnostics, and the second the list of unsuppressed diagnostics.</returns>
13281331
public Tuple<List<SuppressedRecord>, List<DiagnosticRecord>> SuppressRule(
13291332
string ruleName,
13301333
Dictionary<string, List<RuleSuppression>> ruleSuppressionsDict,
13311334
List<DiagnosticRecord> diagnostics,
1332-
out List<ErrorRecord> errorRecords)
1335+
out List<ErrorRecord> errors)
13331336
{
1334-
List<SuppressedRecord> suppressedRecords = new List<SuppressedRecord>();
1335-
List<DiagnosticRecord> unSuppressedRecords = new List<DiagnosticRecord>();
1336-
Tuple<List<SuppressedRecord>, List<DiagnosticRecord>> result = Tuple.Create(suppressedRecords, unSuppressedRecords);
1337-
errorRecords = new List<ErrorRecord>();
1338-
if (diagnostics == null || diagnostics.Count == 0)
1337+
var suppressedRecords = new List<SuppressedRecord>();
1338+
var unsuppressedRecords = new List<DiagnosticRecord>();
1339+
errors = new List<ErrorRecord>();
1340+
var result = new Tuple<List<SuppressedRecord>, List<DiagnosticRecord>>(suppressedRecords, unsuppressedRecords);
1341+
1342+
if (diagnostics is null || diagnostics.Count == 0)
13391343
{
13401344
return result;
13411345
}
13421346

1343-
if (ruleSuppressionsDict == null || !ruleSuppressionsDict.ContainsKey(ruleName)
1344-
|| ruleSuppressionsDict[ruleName].Count == 0)
1347+
if (ruleSuppressionsDict is null
1348+
|| !ruleSuppressionsDict.TryGetValue(ruleName, out List<RuleSuppression> ruleSuppressions)
1349+
|| ruleSuppressions.Count == 0)
13451350
{
1346-
unSuppressedRecords.AddRange(diagnostics);
1351+
unsuppressedRecords.AddRange(diagnostics);
13471352
return result;
13481353
}
13491354

1350-
List<RuleSuppression> ruleSuppressions = ruleSuppressionsDict[ruleName];
1351-
var offsetArr = GetOffsetArray(diagnostics);
1352-
int recordIndex = 0;
1353-
int startRecord = 0;
1354-
bool[] suppressed = new bool[diagnostics.Count];
1355-
foreach (RuleSuppression ruleSuppression in ruleSuppressions)
1355+
// We need to report errors for any rule suppressions with IDs that are not applied to anything
1356+
// Start by assuming all suppressions are unapplied and then we'll remove them as we apply them later
1357+
var unappliedSuppressions = new HashSet<RuleSuppression>();
1358+
foreach (RuleSuppression suppression in ruleSuppressions)
13561359
{
1357-
int suppressionCount = 0;
1358-
while (startRecord < diagnostics.Count
1359-
// && diagnostics[startRecord].Extent.StartOffset < ruleSuppression.StartOffset)
1360-
// && diagnostics[startRecord].Extent.StartLineNumber < ruleSuppression.st)
1361-
&& offsetArr[startRecord] != null && offsetArr[startRecord].Item1 < ruleSuppression.StartOffset)
1360+
if (!string.IsNullOrWhiteSpace(suppression.RuleSuppressionID))
13621361
{
1363-
startRecord += 1;
1362+
unappliedSuppressions.Add(suppression);
13641363
}
1365-
1366-
// at this point, start offset of startRecord is greater or equals to rulesuppression.startoffset
1367-
recordIndex = startRecord;
1368-
1369-
while (recordIndex < diagnostics.Count)
1364+
}
1365+
1366+
// We have suppressions and diagnostics
1367+
// For each diagnostic, collect all the suppressions that apply to it
1368+
// and if there are any, record the diagnostic as suppressed
1369+
foreach (DiagnosticRecord diagnostic in diagnostics)
1370+
{
1371+
Tuple<int, int> offsetPair = GetOffset(diagnostic);
1372+
1373+
var appliedSuppressions = new List<RuleSuppression>();
1374+
foreach (RuleSuppression suppression in ruleSuppressions)
13701375
{
1371-
DiagnosticRecord record = diagnostics[recordIndex];
1372-
var curOffset = offsetArr[recordIndex];
1373-
1374-
//if (record.Extent.EndOffset > ruleSuppression.EndOffset)
1375-
if (curOffset != null && curOffset.Item2 > ruleSuppression.EndOffset)
1376+
// Check that the diagnostic is within the suppressed extent, if we have such an extent
1377+
if (!(offsetPair is null)
1378+
&& (offsetPair.Item1 < suppression.StartOffset || offsetPair.Item2 > suppression.EndOffset))
13761379
{
1377-
break;
1380+
continue;
13781381
}
1379-
1380-
// we suppress if there is no suppression id or if there is suppression id and it matches
1381-
if (string.IsNullOrWhiteSpace(ruleSuppression.RuleSuppressionID)
1382-
|| (!String.IsNullOrWhiteSpace(record.RuleSuppressionID) &&
1383-
string.Equals(ruleSuppression.RuleSuppressionID, record.RuleSuppressionID, StringComparison.OrdinalIgnoreCase)))
1382+
1383+
// If there's a rule suppression ID, check that it matches the diagnostic
1384+
if (!string.IsNullOrWhiteSpace(suppression.RuleSuppressionID)
1385+
&& !string.Equals(diagnostic.RuleSuppressionID, suppression.RuleSuppressionID, StringComparison.OrdinalIgnoreCase))
13841386
{
1385-
suppressed[recordIndex] = true;
1386-
suppressedRecords.Add(new SuppressedRecord(record, ruleSuppression));
1387-
suppressionCount += 1;
1387+
continue;
13881388
}
1389-
1390-
recordIndex += 1;
1389+
1390+
unappliedSuppressions.Remove(suppression);
1391+
appliedSuppressions.Add(suppression);
13911392
}
1392-
1393-
// If we cannot found any error but the rulesuppression has a rulesuppressionid then it must be used wrongly
1394-
if (!String.IsNullOrWhiteSpace(ruleSuppression.RuleSuppressionID) && suppressionCount == 0)
1393+
1394+
if (appliedSuppressions.Count > 0)
13951395
{
1396-
// checks whether are given a string or a file path
1397-
if (String.IsNullOrWhiteSpace(diagnostics.First().Extent.File))
1398-
{
1399-
ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormatScriptDefinition, ruleSuppression.StartAttributeLine,
1400-
String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID));
1401-
}
1402-
else
1403-
{
1404-
ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSuppression.StartAttributeLine,
1405-
System.IO.Path.GetFileName(diagnostics.First().Extent.File), String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID));
1406-
}
1407-
errorRecords.Add(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression));
1408-
//this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression));
1396+
suppressedRecords.Add(new SuppressedRecord(diagnostic, appliedSuppressions));
1397+
}
1398+
else
1399+
{
1400+
unsuppressedRecords.Add(diagnostic);
14091401
}
14101402
}
1411-
1412-
for (int i = 0; i < suppressed.Length; i += 1)
1403+
1404+
// Do any error reporting for misused RuleSuppressionIDs here.
1405+
// If the user applied a suppression qualified by a suppression ID,
1406+
// but that suppression didn't apply only because the suppression ID didn't match
1407+
// we let the user know in the form of an error.
1408+
string analyzedFile = diagnostics[0]?.Extent?.File;
1409+
bool appliedToFile = !string.IsNullOrEmpty(analyzedFile);
1410+
string analyzedFileName = appliedToFile ? Path.GetFileName(analyzedFile) : null;
1411+
foreach (RuleSuppression unappliedSuppression in unappliedSuppressions)
14131412
{
1414-
if (!suppressed[i])
1413+
string suppressionIDError = string.Format(Strings.RuleSuppressionIDError, unappliedSuppression.RuleSuppressionID);
1414+
1415+
if (appliedToFile)
14151416
{
1416-
unSuppressedRecords.Add(diagnostics[i]);
1417+
unappliedSuppression.Error = string.Format(
1418+
CultureInfo.CurrentCulture,
1419+
Strings.RuleSuppressionErrorFormat,
1420+
unappliedSuppression.StartAttributeLine,
1421+
analyzedFileName,
1422+
suppressionIDError);
14171423
}
1424+
else
1425+
{
1426+
unappliedSuppression.Error = string.Format(
1427+
CultureInfo.CurrentCulture,
1428+
Strings.RuleSuppressionErrorFormatScriptDefinition,
1429+
unappliedSuppression.StartAttributeLine,
1430+
suppressionIDError);
1431+
}
1432+
1433+
errors.Add(
1434+
new ErrorRecord(
1435+
new ArgumentException(unappliedSuppression.Error),
1436+
unappliedSuppression.Error,
1437+
ErrorCategory.InvalidArgument,
1438+
unappliedSuppression));
14181439
}
14191440

14201441
return result;
14211442
}
14221443

1423-
private Tuple<int,int>[] GetOffsetArray(List<DiagnosticRecord> diagnostics)
1444+
private Tuple<int, int> GetOffset(DiagnosticRecord diagnostic)
14241445
{
1425-
Func<int,int,Tuple<int,int>> GetTuple = (x, y) => new Tuple<int, int>(x, y);
1426-
Func<Tuple<int, int>> GetDefaultTuple = () => GetTuple(0, 0);
1427-
var offsets = new Tuple<int, int>[diagnostics.Count];
1428-
for (int k = 0; k < diagnostics.Count; k++)
1446+
IScriptExtent extent = diagnostic.Extent;
1447+
1448+
if (extent is null)
14291449
{
1430-
var ext = diagnostics[k].Extent;
1431-
if (ext == null)
1432-
{
1433-
continue;
1434-
}
1435-
if (ext.StartOffset == 0 && ext.EndOffset == 0)
1450+
return null;
1451+
}
1452+
1453+
if (extent.StartOffset != 0 && extent.EndOffset != 0)
1454+
{
1455+
return Tuple.Create(extent.StartOffset, extent.EndOffset);
1456+
}
1457+
1458+
// We're forced to determine the offset ourselves from the lines and columns now
1459+
// Rather than counting every line in the file, we scan through the tokens looking for ones matching the offset
1460+
1461+
Token startToken = null;
1462+
int i = 0;
1463+
for (; i < Tokens.Length; i++)
1464+
{
1465+
Token curr = Tokens[i];
1466+
if (curr.Extent.StartLineNumber == extent.StartLineNumber
1467+
&& curr.Extent.StartColumnNumber == extent.StartColumnNumber)
14361468
{
1437-
// check if line and column number correspond to 0 offsets
1438-
if (ext.StartLineNumber == 1
1439-
&& ext.StartColumnNumber == 1
1440-
&& ext.EndLineNumber == 1
1441-
&& ext.EndColumnNumber == 1)
1442-
{
1443-
offsets[k] = GetDefaultTuple();
1444-
continue;
1445-
}
1446-
// created using the ScriptExtent constructor, which sets
1447-
// StartOffset and EndOffset to 0
1448-
// find the token the corresponding start line and column number
1449-
var startToken = Tokens.Where(x
1450-
=> x.Extent.StartLineNumber == ext.StartLineNumber
1451-
&& x.Extent.StartColumnNumber == ext.StartColumnNumber)
1452-
.FirstOrDefault();
1453-
if (startToken == null)
1454-
{
1455-
offsets[k] = GetDefaultTuple();
1456-
continue;
1457-
}
1458-
var endToken = Tokens.Where(x
1459-
=> x.Extent.EndLineNumber == ext.EndLineNumber
1460-
&& x.Extent.EndColumnNumber == ext.EndColumnNumber)
1461-
.FirstOrDefault();
1462-
if (endToken == null)
1463-
{
1464-
offsets[k] = GetDefaultTuple();
1465-
continue;
1466-
}
1467-
offsets[k] = GetTuple(startToken.Extent.StartOffset, endToken.Extent.EndOffset);
1469+
startToken = curr;
1470+
break;
14681471
}
1469-
else
1472+
}
1473+
1474+
if (startToken is null)
1475+
{
1476+
return Tuple.Create(0, 0);
1477+
}
1478+
1479+
Token endToken = null;
1480+
for (; i < Tokens.Length; i++)
1481+
{
1482+
Token curr = Tokens[i];
1483+
if (curr.Extent.EndLineNumber == extent.EndLineNumber
1484+
&& curr.Extent.EndColumnNumber == extent.EndColumnNumber)
14701485
{
1471-
// Extent has valid offsets
1472-
offsets[k] = GetTuple(ext.StartOffset, ext.EndOffset);
1486+
endToken = curr;
1487+
break;
14731488
}
14741489
}
1475-
return offsets;
1490+
1491+
if (endToken is null)
1492+
{
1493+
return Tuple.Create(0, 0);
1494+
}
1495+
1496+
return Tuple.Create(startToken.Extent.StartOffset, endToken.Extent.EndOffset);
14761497
}
14771498

14781499
public static string[] ProcessCustomRulePaths(string[] rulePaths, SessionState sessionState, bool recurse = false)

0 commit comments

Comments
 (0)