这是第二篇文章,重点介绍了在云CI系统中PVS-Studio分析仪的用法。 这次,我们将考虑平台Azure DevOps-Microsoft的云CI \ CD解决方案。 我们将分析ShareX项目。
我们将需要三个组件。 首先是PVS-Studio分析仪。 第二个是Azure DevOps,我们将其与分析器集成在一起。 第三个是我们将检查的项目,以演示在云中工作时PVS-Studio的功能。 因此,让我们开始吧。
PVS-Studio是用于发现错误和安全缺陷的静态代码分析器。 该工具支持对C,C ++和C#代码的分析。
Azure DevOps 。 Azure DevOps平台包括诸如Azure Pipeline,Azure Board,Azure Artifacts之类的工具,这些工具可加快创建软件的过程并提高其质量。
ShareX是一个免费的应用程序,可让您捕获和记录屏幕的任何部分。 该项目用C#编写,非常适合显示静态分析器启动的配置。 该项目的源代码
可在GitHub上获得 。
ShareX项目的cloc命令的输出:
换句话说,该项目很小,但是足以演示PVS-Studio和云平台的工作。
让我们开始配置
要开始在Azure DevOps中工作,请点击
链接 ,然后按“使用GitHub免费开始”。
向Microsoft应用程序授予对GitHub帐户数据的访问权限。
您必须创建一个Microsoft帐户才能完成注册。
注册后,创建一个项目:
接下来,我们需要转到“管道”-“构建”并创建一个新的构建管道。
当被问及我们的代码位于何处时,我们将回答-GitHub。
授权Azure Pipelines并选择项目的存储库,我们将为其配置静态分析器的运行。
在模板选择窗口中,选择“启动程序管道”。
我们可以通过两种方式对项目进行静态代码分析:使用Microsoft托管或自托管代理。
首先,我们将使用Microsoft托管的代理。 这些代理是在运行管道时启动的普通虚拟机。 完成任务后将它们删除。 使用此类代理使我们不会浪费时间来支持和更新它们,而是施加了某些限制,例如-无法安装用于构建项目的其他软件。
让我们将以下建议的默认配置替换为以下使用Microsoft托管代理的默认配置:
# Setting up run triggers # Run only for changes in the master branch trigger: - master # Since the installation of random software in virtual machines # is prohibited, we'll use a Docker container, # launched on a virtual machine with Windows Server 1803 pool: vmImage: 'win1803' container: microsoft/dotnet-framework:4.7.2-sdk-windowsservercore-1803 steps: # Download the analyzer distribution - task: PowerShell@2 inputs: targetType: 'inline' script: 'Invoke-WebRequest -Uri https://files.viva64.com/PVS-Studio_setup.exe -OutFile PVS-Studio_setup.exe' - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | # Restore the project and download dependencies nuget restore .\ShareX.sln # Create the directory, where files with analyzer reports will be saved md .\PVSTestResults # Install the analyzer PVS-Studio_setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /COMPONENTS=Core # Create the file with configuration and license information "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" credentials -u $(PVS_USERNAME) -n $(PVS_KEY) # Run the static analyzer and convert the report in html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog # Save analyzer reports - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults
注意:根据
文档 ,使用的容器必须缓存在虚拟机的映像中,但是在撰写本文时,它不起作用,并且每次任务启动时都会下载该容器,这对执行时间。
让我们保存管道并创建用于创建许可证文件的变量。 为此,请打开管道编辑窗口,然后单击右上角的“变量”。
然后,添加两个变量
-PVS_USERNAME和
PVS_KEY ,分别包含用户名和许可证密钥。 创建
PVS_KEY变量时,请不要忘记选择“保留此值密码”以使用2048位RSA密钥加密变量的值,并在任务性能日志中禁止输出变量值。
保存变量并通过单击“运行”运行管道。
运行分析的第二个选项-使用自托管代理。 我们可以自己定制和管理自托管代理。 此类代理为构建和测试我们的软件产品所需的软件提供了更多的安装机会。
在使用此类代理之前,您必须根据
说明进行配置,并安装和
配置静态分析器。
要在自托管代理上运行任务,我们将使用以下内容替换建议的配置:
# Setting up triggers # Run the analysis for master-branch trigger: - master # The task is run on a self-hosted agent from the pool 'MyPool' pool: 'MyPool' steps: - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | # Restore the project and download dependencies nuget restore .\ShareX.sln # Create the directory where files with analyzer reports will be saved md .\PVSTestResults # Run the static analyzer and convert the report in html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog # Save analyzer reports - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults
任务完成后,您可以在“摘要”标签下下载带有分析器报告的存档,也可以使用扩展
邮件发送邮件来配置电子邮件或考虑
Marketplace上的另一种便捷工具。
分析结果
现在,让我们看看在测试项目ShareX中发现的一些错误。
过多检查为了进行预热,让我们从代码中的简单缺陷开始,即从冗余检查开始:
private void PbThumbnail_MouseMove(object sender, MouseEventArgs e) { .... IDataObject dataObject = new DataObject(DataFormats.FileDrop, new string[] { Task.Info.FilePath }); if (dataObject != null) { Program.MainForm.AllowDrop = false; dragBoxFromMouseDown = Rectangle.Empty; pbThumbnail.DoDragDrop(dataObject, DragDropEffects.Copy | DragDropEffects.Move); Program.MainForm.AllowDrop = true; } .... }
PVS-Studio警告: V3022 [CWE-571]表达式'dataObject!= Null'始终为true。 TaskThumbnailPanel.cs 415
让我们注意检查
dataObject变量是否为
null 。 为什么在这里? 在这种情况下,
dataObject不能为
null ,因为它是由对创建的对象的引用初始化的。 结果,我们进行了过多的检查。 关键? 不行 看起来简洁吗? 不行 清除该检查显然更好,以免使代码混乱。
让我们看一下我们可以用类似方式注释的另一段代码:
private static Image GetDIBImage(MemoryStream ms) { .... try { .... return new Bitmap(bmp); .... } finally { if (gcHandle != IntPtr.Zero) { GCHandle.FromIntPtr(gcHandle).Free(); } } .... } private static Image GetImageAlternative() { .... using (MemoryStream ms = dataObject.GetData(format) as MemoryStream) { if (ms != null) { try { Image img = GetDIBImage(ms); if (img != null) { return img; } } catch (Exception e) { DebugHelper.WriteException(e); } } } .... }
PVS-Studio警告: V3022 [CWE-571]表达式'img!= Null'始终为true。 ClipboardHelpers.cs 289
在
GetImageAlternative方法中,在创建
Bitmap类的新实例之后,立即检查
img变量是否不为null。 与上一个示例的区别在于,我们使用
GetDIBImage方法而不是构造函数来初始化
img变量。 代码作者建议此方法可能会发生异常,但他只声明
try和
finally块,而忽略
catch 。 因此,如果发生异常,则调用者方法
GetImageAlternative将不会获取对
Bitmap类型的对象的引用,而是必须在其自己的
catch块中处理该异常。 在这种情况下,将不会初始化
img变量,并且执行线程甚至不会到达
img!= Null检查,但将进入catch块。 因此,分析仪确实指出了过度检查。
让我们考虑以下
V3022警告示例:
private void btnCopyLink_Click(object sender, EventArgs e) { .... if (lvClipboardFormats.SelectedItems.Count == 0) { url = lvClipboardFormats.Items[0].SubItems[1].Text; } else if (lvClipboardFormats.SelectedItems.Count > 0) { url = lvClipboardFormats.SelectedItems[0].SubItems[1].Text; } .... }
PVS-Studio警告: V3022 [CWE-571]表达式'lvClipboardFormats.SelectedItems.Count> 0'始终为true。 AfterUploadForm.cs 155
让我们仔细看看第二个条件表达式。 在那里,我们检查只读
Count属性的值。 此属性显示集合
SelectedItems的实例中的元素数。 仅当
Count属性大于零时才执行该条件。 一切都很好,但是在外部
if语句中
Count已经被检查为0。SelectedItems集合的实例的元素数不能小于零,因此
Count等于或大于0。已经在第一个
if语句中执行了0的
Count检查并且它为false,因此没有必要在else分支中编写另一个大于零的
Count检查。
V3022警告的最终示例将是以下代码片段:
private void DrawCursorGraphics(Graphics g) { .... int cursorOffsetX = 10, cursorOffsetY = 10, itemGap = 10, itemCount = 0; Size totalSize = Size.Empty; int magnifierPosition = 0; Bitmap magnifier = null; if (Options.ShowMagnifier) { if (itemCount > 0) totalSize.Height += itemGap; .... } .... }
PVS-Studio警告: V3022表达式'itemCount> 0'始终为false。 RegionCaptureForm.cs 1100
分析器注意到,条件
itemCount> 0始终为false,因为声明了
itemCount变量,并且在上面同时将其分配为零。 此变量在任何情况下都不会使用,因此分析器对条件表达式的判断正确,其值始终为false。
好吧,现在让我们看一下确实很简单的东西。
理解错误的最好方法是可视化错误在我们看来,在这个地方发现了一个相当有趣的错误:
public static void Pixelate(Bitmap bmp, int pixelSize) { .... float r = 0, g = 0, b = 0, a = 0; float weightedCount = 0; for (int y2 = y; y2 < yLimit; y2++) { for (int x2 = x; x2 < xLimit; x2++) { ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight; } } .... ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount)); .... }
我不想展示所有卡,也不愿透露分析仪的发现,所以让我们搁置一会儿。
通过该方法的名称,很容易猜出它在做什么-给它提供图像或图像片段,然后将其像素化。 该方法的代码很长,因此我们不会完全引用它,而只是尝试解释其算法并解释PVS-Studio设法找到了哪种错误。
此方法接收两个参数:
位图类型的对象和指示像素大小的
int类型的值。 操作算法非常简单:
1)将接收到的图像片段分成正方形,边长等于像素大小。 例如,如果像素大小等于15,我们将得到一个正方形,包含15x15 = 225个像素。
2)此外,我们遍历此正方形中的每个像素,并在中间变量中累加字段
Red ,
Green ,
Blue和
Alpha的值,然后再将相应颜色和alpha通道的值乘以
pixelWeight变量,将
Alpha值除以255(
Alpha变量为
字节类型)。 同样,当遍历像素时,我们对这些值求和,以
pixelWeight形式写入
weightedCount变量。 执行上述操作的代码片段如下:
ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight;
顺便说一句,请注意,如果
Alpha变量的值为零,则
pixelWeight不会将该像素的任何值添加到
weightedCount变量。 我们将来会需要。
3)遍历当前正方形中的所有像素后,我们可以为该正方形创建通用的“平均”颜色。 这样做的代码如下所示:
ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount));
4)现在,当我们获得最终的颜色并将其
写入averageColor变量中时,我们可以再次遍历正方形的每个像素,并从
averageColor为其分配一个值。
5)当我们有未处理的正方形时,回到点2。
再一次,
weightedCount变量不等于一个正方形中所有像素的数量。 例如,如果图像包含一个完全透明的像素(alpha通道中的值为零),则该像素的
pixelWeight变量将为零(
0/255 = 0)。 因此,此像素不会影响
weightedCount变量的形成。 这是很合逻辑的-没有必要考虑完全透明像素的颜色。
因此,一切似乎都合理-像素化必须正常工作。 实际上是这样。 这仅适用于包含像素的png图像,该像素的alpha通道中的值小于255且不等于零。 注意下面的像素化图片:
你看过像素化了吗? 我们也没有。 好的,现在让我们揭示一下这个小技巧,并说明该方法将错误确切地隐藏在什么地方。 该错误
蔓延到
pixelWeight变量计算的行:
float pixelWeight = color.Alpha / 255;
事实的事实是,当将
pixelWeight变量声明为
float时 ,代码作者暗示说,当将
Alpha字段除以255时,除零和一外,他还将获得分数。 这就是问题所在,因为
Alpha变量是
字节类型。 将其除以255时,我们得到一个整数值。 只有在此之后,它才会隐式转换为
float类型,这意味着小数部分会丢失。
很容易解释为什么无法以某种透明度对png图像进行像素化。 因为对于这些像素,alpha通道的值在0 <Alpha <255范围内,所以
Alpha变量除以255将始终为0。因此,变量
pixelWeight ,
r ,
g ,
b ,
a ,
weightedCount的值也将始终为0。因此,我们的
averageColor在所有通道中的值为零:红色-0,蓝色-0,绿色-0,alpha-0。通过以这种颜色绘制正方形,我们不会更改原始颜色像素,因为
averageColor是绝对透明的。 要解决此错误,我们只需要将
Alpha字段显式转换为
float类型即可。 固定版本的代码行可能如下所示:
float pixelWeight = (float)color.Alpha / 255;
好了,是时候为错误的代码引用PVS-Studio的消息了:
PVS-Studio警告: V3041 [CWE-682]表达式从'int'类型隐式转换为'float'类型。 考虑使用显式类型转换以避免丢失小数部分。 例如:double A =(double)(X)/ Y;。 ImageHelpers.cs 1119
为了进行比较,让我们举一个在校正后的应用程序版本上获得的真实像素化图像的屏幕截图:
潜在的NullReferenceException public static bool AddMetadata(Image img, int id, string text) { .... pi.Value = bytesText; if (pi != null) { img.SetPropertyItem(pi); return true; } .... }
PVS-Studio警告: V3095 [CWE-476]在对null进行验证之前使用了'pi'对象。 检查行:801、803。ImageHelpers.cs 801
此代码段显示作者希望
pi变量可以为
null ,这就是为什么在调用方法
SetPropertyItem之前执行
pi!= Null的原因 。 奇怪的是,在此检查之前,为属性分配了一个字节数组,因为如果
pi为
null ,则将引发
NullReferenceException类型的异常。
在另一个地方也发现了类似的情况:
private static void Task_TaskCompleted(WorkerTask task) { .... task.KeepImage = false; if (task != null) { if (task.RequestSettingUpdate) { Program.MainForm.UpdateCheckStates(); } .... } .... }
PVS-Studio警告: V3095 [CWE-476]在验证为空之前使用了“任务”对象。 检查行:268,270。TaskManager.cs 268
PVS-Studio发现另一个类似的错误。 重点是一样的,因此不需要引用代码片段,分析器消息就足够了。
PVS-Studio警告: V3095 [CWE-476]在对null进行验证之前,已使用'Config.PhotobucketAccountInfo'对象。 检查行:216,219。UploadersConfigForm.cs 216
相同的返回值在
WindowsList类的
EvalWindows方法中发现了可疑的代码片段,该代码片段在所有情况下均返回
true :
public class WindowsList { public List<IntPtr> IgnoreWindows { get; set; } .... public WindowsList() { IgnoreWindows = new List<IntPtr>(); } public WindowsList(IntPtr ignoreWindow) : this() { IgnoreWindows.Add(ignoreWindow); } .... private bool EvalWindows(IntPtr hWnd, IntPtr lParam) { if (IgnoreWindows.Any(window => hWnd == window)) { return true;
PVS-Studio警告: V3009奇怪的是,此方法始终返回一个相同的“ true”值。 WindowsList.cs 82
从逻辑
上讲 ,如果在名为
IgnoreWindows的列表中有一个与
hWnd同名的指针,则该方法必须返回
false 。
可以在调用构造函数
WindowsList(IntPtr ignoreWindow)时直接填充
IgnoreWindows列表,也可以直接通过访问公共属性来填充
IgnoreWindows列表。 无论如何,根据Visual Studio,目前在代码中此列表尚未填充。 这是这种方法的另一个奇怪的地方。
事件处理程序的不安全调用 protected void OnNewsLoaded() { if (NewsLoaded != null) { NewsLoaded(this, EventArgs.Empty); } }
PVS-Studio警告: V3083 [CWE-367]事件'NewsLoaded'的不安全调用,可能会发生NullReferenceException。 请考虑在调用事件之前将事件分配给局部变量。 NewsListControl.cs 111
在这里,可能发生非常讨厌的情况。 检查
NewsLoaded变量是否为null之后,可以取消订阅处理事件的方法,例如,在另一个线程中。 在这种情况下,当我们进入if语句的主体时,变量
NewsLoaded将已经为空。 尝试从事件
NewsLoaded调用订户时,可能会发生
NullReferenceException ,该事件为null。 使用空条件运算符并按如下所示重写上面的代码要安全得多:
protected void OnNewsLoaded() { NewsLoaded?.Invoke(this, EventArgs.Empty); }
分析仪指出了
68个类似的碎片。 我们不会全部描述它们-它们都有相似的调用模式。
从ToString返回null最近,我从我同事的
一篇有趣的文章中发现,Microsoft不建议从重写的方法
ToString返回null。 PVS-Studio对此非常了解:
public override string ToString() { lock (loggerLock) { if (sbMessages != null && sbMessages.Length > 0) { return sbMessages.ToString(); } return null; } }
PVS-Studio警告: V3108不建议从“ ToSting()”方法返回“ null”。 Logger.cs 167
如果不使用,为什么要分配? public SeafileCheckAccInfoResponse GetAccountInfo() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "account/info/?format=json"); .... }
PVS-Studio警告: V3008为 'url'变量连续两次分配值。 也许这是一个错误。 检查行:197,196。Seafile.cs 197
从示例中可以看出,在声明
url变量时,将为其分配一个值,该值是从
FixPrefix方法
返回的 。 在下一行中,即使没有在任何地方使用它,我们也会清除获得的值。 我们得到了类似于无效代码的东西:它可以工作,但不会影响结果。 此错误最有可能是复制粘贴的结果,因为这样的代码片段在另外9种方法中发生。 举个例子,我们将举两个第一行相似的方法:
public bool CheckAuthToken() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "auth/ping/?format=json"); .... } .... public bool CheckAPIURL() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "ping/?format=json"); .... }
结论
如我们所见,自动分析仪检查的配置复杂性不取决于所选的CI系统。 从字面上看,我们花了15分钟时间和几次鼠标单击来配置使用静态分析器对项目代码的检查。
最后,我们邀请您
下载并在您的项目上
试用分析仪 。