Skip to content

Commit fb82d09

Browse files
authored
Fix rewriting files with no issues to suppress (#632)
* Fix rewriting files with no issues to suppress Fixes an issue where the Suppress command would rewrite a file essentially as is (but potentially change whitespace) even if all the detected issues in the file were filtered out by the rule filter. * Respect original newline characters when adding suppression comments * Rewrite edge case handling for placing multi line comments at the start of the line when the line ends with `\` and might be a multi-line continuation to avoid putting the comment inside the multiline string. * Add tests cases for fixes * Fix readme typo from #628
1 parent 7e08341 commit fb82d09

File tree

4 files changed

+143
-11
lines changed

4 files changed

+143
-11
lines changed

Changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.0.42] - 2024-08-26
8+
## Fix
9+
Fixes suppression command to not perturb line breaks, particularly when a file has findings which are not selected for suppression. #631
10+
711
## [1.0.41] - 2024-08-23
812
## Rules
913
Extend the false positive fix for the issue reported in #548 to Sdk-style msbuild projects.

DevSkim-DotNet/Microsoft.DevSkim.CLI/Commands/SuppressionCommand.cs

+80-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (C) Microsoft. All rights reserved. Licensed under the MIT License.
22

33
using System;
4+
using System.Collections.Generic;
45
using System.IO;
56
using System.Linq;
67
using System.Text;
@@ -61,6 +62,12 @@ public int Run()
6162
issueRecords = issueRecords.Where(x => _opts.RulesToApplyFrom.Any(y => y == x.RuleId)).ToList();
6263
}
6364

65+
// No issues remain for file after filtering to specified rule ids, skip file
66+
if (!issueRecords.Any())
67+
{
68+
continue;
69+
}
70+
6471
issueRecords
6572
.Sort((a, b) => a.PhysicalLocation.Region.StartLine - b.PhysicalLocation.Region.StartLine);
6673

@@ -75,9 +82,10 @@ public int Run()
7582
if (!File.Exists(potentialPath))
7683
{
7784
_logger.LogError($"{potentialPath} specified in sarif does not appear to exist on disk.");
85+
continue;
7886
}
79-
80-
string[] theContent = File.ReadAllLines(potentialPath);
87+
string content = File.ReadAllText(potentialPath);
88+
string[] theContent = SplitStringByLinesWithNewLines(content);
8189
int currLine = 0;
8290
StringBuilder sb = new StringBuilder();
8391

@@ -87,18 +95,36 @@ public int Run()
8795
{
8896
Region region = issueRecord.PhysicalLocation.Region;
8997
int zeroBasedStartLine = region.StartLine - 1;
90-
bool isMultiline = theContent[zeroBasedStartLine].EndsWith(@"\");
91-
string ignoreComment = DevSkimRuleProcessor.GenerateSuppressionByLanguage(region.SourceLanguage, issueRecord.RulesId, _opts.PreferMultiline || isMultiline, _opts.Duration, _opts.Reviewer, devSkimLanguages);
98+
string originalLine = theContent[zeroBasedStartLine];
99+
int lineEndPosition = FindNewLine(originalLine);
100+
// If line ends with `\` it may have continuation on the next line,
101+
// so put the comment at the line start and use multiline format
102+
bool forceMultiLine = lineEndPosition >= 0 && originalLine[..lineEndPosition].EndsWith(@"\");
103+
string ignoreComment = DevSkimRuleProcessor.GenerateSuppressionByLanguage(region.SourceLanguage, issueRecord.RulesId, _opts.PreferMultiline || forceMultiLine, _opts.Duration, _opts.Reviewer, devSkimLanguages);
92104
if (!string.IsNullOrEmpty(ignoreComment))
93105
{
94106
foreach (string line in theContent[currLine..zeroBasedStartLine])
95107
{
96-
sb.Append($"{line}{Environment.NewLine}");
108+
sb.Append($"{line}");
109+
}
110+
111+
if (forceMultiLine)
112+
{
113+
sb.Append($"{ignoreComment} {originalLine}");
114+
}
115+
else
116+
{
117+
// Use the content then the ignore comment then the original newline characters from the extra array
118+
if (lineEndPosition != -1)
119+
{
120+
sb.Append($"{originalLine[0..lineEndPosition]} {ignoreComment}{originalLine[lineEndPosition..]}");
121+
}
122+
// No new line so we can just use the line as is
123+
else
124+
{
125+
sb.Append($"{originalLine} {ignoreComment}");
126+
}
97127
}
98-
99-
string suppressionComment = isMultiline ? $"{ignoreComment}{theContent[zeroBasedStartLine]}{Environment.NewLine}" :
100-
$"{theContent[zeroBasedStartLine]} {ignoreComment}{Environment.NewLine}";
101-
sb.Append(suppressionComment);
102128
}
103129

104130
currLine = zeroBasedStartLine + 1;
@@ -109,7 +135,7 @@ public int Run()
109135
{
110136
foreach (string line in theContent[currLine..^1])
111137
{
112-
sb.Append($"{line}{Environment.NewLine}");
138+
sb.Append($"{line}");
113139
}
114140
sb.Append($"{theContent.Last()}");
115141
}
@@ -127,5 +153,49 @@ public int Run()
127153

128154
return (int)ExitCode.NoIssues;
129155
}
156+
157+
/// <summary>
158+
/// Find the first location of a newline (\n or \r\n) in a string
159+
/// </summary>
160+
/// <param name="originalLine"></param>
161+
/// <returns>Character index of the first newline sequence or -1 if none found</returns>
162+
private int FindNewLine(string originalLine)
163+
{
164+
int indexOfNewLine = originalLine.IndexOf('\n');
165+
if (indexOfNewLine >= 1)
166+
{
167+
if (originalLine[indexOfNewLine - 1] == '\r')
168+
{
169+
indexOfNewLine = indexOfNewLine - 1;
170+
}
171+
}
172+
173+
return indexOfNewLine;
174+
}
175+
176+
/// <summary>
177+
/// Split string into lines including the newline characters
178+
/// </summary>
179+
/// <param name="content"></param>
180+
/// <returns>Array of strings, each containing the content of one line from the input string including any newline characters from that line</returns>
181+
private string[] SplitStringByLinesWithNewLines(string content)
182+
{
183+
List<string> lines = new();
184+
int curPos = 0;
185+
for (int i = 0; i < content.Length; i++)
186+
{
187+
if (content[i] == '\n')
188+
{
189+
lines.Add(content[curPos..(i+1)]);
190+
curPos = i + 1;
191+
}
192+
193+
if (i == content.Length - 1)
194+
{
195+
lines.Add(content[curPos..(i+1)]);
196+
}
197+
}
198+
return lines.ToArray();
199+
}
130200
}
131201
}

DevSkim-DotNet/Microsoft.DevSkim.Tests/SuppressionsTest.cs

+58
Original file line numberDiff line numberDiff line change
@@ -286,5 +286,63 @@ public void ExecuteSuppresionsForMultilineFormattedFiles(bool preferMultiLine)
286286
new AnalyzeCommand(opts).Run();
287287
return (basePath, tempFileName, Path.Combine(basePath, outFileName));
288288
}
289+
290+
/// <summary>
291+
/// Test that suppressing an issue doesn't change the line break characters
292+
/// </summary>
293+
/// <param name="lineBreakSequence">Character sequence used for line breaks</param>
294+
/// <param name="preferMultiLine">Use multiline comments or not</param>
295+
[DataTestMethod]
296+
[DataRow("\r\n", true)]
297+
[DataRow("\r\n", false)]
298+
[DataRow("\n", true)]
299+
[DataRow("\n", false)]
300+
public void DontPerturbExtantLineBreaks(string lineBreakSequence, bool preferMultiLine)
301+
{
302+
(string basePath, string sourceFile, string sarifPath) = runAnalysis($"MD5\\{lineBreakSequence}http://contoso.com{lineBreakSequence}", "c");
303+
304+
SuppressionCommandOptions opts = new SuppressionCommandOptions
305+
{
306+
Path = basePath,
307+
SarifInput = sarifPath,
308+
ApplyAllSuppression = true,
309+
PreferMultiline = preferMultiLine
310+
};
311+
312+
int resultCode = new SuppressionCommand(opts).Run();
313+
Assert.AreEqual(0, resultCode);
314+
string result = File.ReadAllText(sourceFile);
315+
Assert.AreEqual(lineBreakSequence, result[^lineBreakSequence.Length..]);
316+
}
317+
318+
/// <summary>
319+
/// Test that files don't change at all when they have findings but those rule ids are not selected for suppression
320+
/// </summary>
321+
/// <param name="lineBreakSequence">Character sequence used for line breaks</param>
322+
/// <param name="preferMultiLine">Use multiline comments or not</param>
323+
[DataTestMethod]
324+
[DataRow("\r\n", true)]
325+
[DataRow("\r\n", false)]
326+
[DataRow("\n", true)]
327+
[DataRow("\n", false)]
328+
public void DontChangeFilesWithoutSelectedFindings(string lineBreakSequence, bool preferMultiline)
329+
{
330+
string originalContent = $"MD5{lineBreakSequence}http://contoso.com{lineBreakSequence}";
331+
(string basePath, string sourceFile, string sarifPath) = runAnalysis(originalContent, "c");
332+
333+
SuppressionCommandOptions opts = new SuppressionCommandOptions
334+
{
335+
Path = basePath,
336+
SarifInput = sarifPath,
337+
ApplyAllSuppression = false,
338+
PreferMultiline = preferMultiline,
339+
RulesToApplyFrom = new string[] { "NotAValidRuleId" } // Don't apply any rules
340+
};
341+
342+
int resultCode = new SuppressionCommand(opts).Run();
343+
Assert.AreEqual(0, resultCode);
344+
string result = File.ReadAllText(sourceFile);
345+
Assert.AreEqual(originalContent, result);
346+
}
289347
}
290348
}

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The Visual Studio extension is available in the [Visual Studio Marketplace](http
5151

5252
The Visual Studio Code plugin is available in the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=MS-CST-E.vscode-devskim).
5353

54-
DevSkim is also available as a [GitHub Action](https://github.com/microsoft/DevSkim-Action) to itegrate with the GitHub Security Issues pane.
54+
DevSkim is also available as a [GitHub Action](https://github.com/microsoft/DevSkim-Action) to integrate with the GitHub Security Issues pane.
5555

5656
Platform specific binaries of the DevSkim CLI are also available on our GitHub [releases page](https://github.com/microsoft/DevSkim/releases).
5757

0 commit comments

Comments
 (0)