Infer.NET代码中隐藏了哪些错误?


微软发布其项目的源代码是验证它们的一个很好的理由。 这次也不例外,今天我们看一下在Infer.NET代码中发现的可疑位置。 向下注解-直达重点!

关于项目和分析器的一些知识


Infer.NET是由Microsoft的专家开发的机器学习系统。 该项目的源代码最近已在GitHub上提供 ,这就是进行验证的原因。 例如,可以在此处找到有关该项目的更多详细信息。

使用PVS-Studio版本6.26静态分析器检查了该项目。 让我提醒您,PVS-Studio正在Windows,Linux,macOS下的C \ C ++ \ C#(以及Java中)中寻找代码错误。 到目前为止,我们仅在Windows下分析C#代码。 可以下载分析仪在您的项目上试用

检查本身非常简单,没有问题。 以前,我从GitHub上卸载了该项目,还原了所需的软件包(依赖项),并确保该项目已成功构建。 这是必需的,以便分析仪可以访问所有必要的信息以进行全面分析。 单击几下后,我通过用于Visual Studio的PVS-Studio插件启动了对解决方案的分析。

顺便说一下,这不是Microsoft使用PVS-Studio测试的第一个项目-还有其他项目: RoslynMSBuildPowerShellCoreFX

注意事项 如果您或您的熟人对分析Java代码感兴趣,可以通过选择“我想要Java分析器”来给我们写信以获取支持 。 该分析器没有公开的Beta版本,但应尽快提供。 他们在秘密实验室的某个地方(穿过墙壁)正在积极地工作。

但是足够多的抽象讨论-让我们看一下代码中的问题。

这是错误还是功能?


我建议您自己尝试查找错误-完全可以解决的任务。 老实说,我不会以“ 2017年C ++项目中的十大错误 ”一文笑。 因此,请勿急于阅读代码段后提供的分析器警告。

private void MergeParallelTransitions() { .... if ( transition1.DestinationStateIndex == transition2.DestinationStateIndex && transition1.Group == transition2.Group) { if (transition1.IsEpsilon && transition2.IsEpsilon) { .... } else if (!transition1.IsEpsilon && !transition2.IsEpsilon) { .... if (double.IsInfinity(transition1.Weight.Value) && double.IsInfinity(transition1.Weight.Value)) { newElementDistribution.SetToSum( 1.0, transition1.ElementDistribution, 1.0, transition2.ElementDistribution); } else { newElementDistribution.SetToSum( transition1.Weight.Value, transition1.ElementDistribution, transition2.Weight.Value, transition2.ElementDistribution); } .... } 

PVS-Studio警告V3001在'&&'运算符的左侧和右侧有相同的子表达式'double.IsInfinity(transition1.Weight.Value)'。 运行时自动机.Simplification.cs 479

从代码片段中可以看到,该方法正在使用一对变量-transition1transition2 。 有时使用相似名称是合理的,但是值得记住的是,在这种情况下,使用该名称在某处意外犯错的可能性会增加。

这是检查数字的无穷大( double.IsInfinity )时发生的情况。 由于错误,我们两次检查了相同变量的值-transition1.Weight.Value 。 第二个子表达式中的检查值应为变量transition2.Weight.Value

另一个类似的可疑代码。

 internal MethodBase ToMethodInternal(IMethodReference imr) { .... bf |= BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; .... } 

PVS-Studio警告V3001在'|'的左侧和右侧有相同的子表达式'BindingFlags.Public' 操作员。 编译器CodeBuilder.cs 194

形成bf变量的值时,将BindingFlags.Public枚举元素使用两次。 要么此代码包含额外的标记操作,要么代替第二次使用BindingFlags.Public,应该使用不同的枚举值。

顺便说一下,在源代码中,此代码写在一行上。 在我看来,如果将其格式化为表格样式(如此处所示),则更容易发现问题。

让我们继续前进。 我介绍了该方法的全部内容,并再次建议您自己发现错误(或者可能是错误)。

 private void ForEachPrefix(IExpression expr, Action<IExpression> action) { // This method must be kept consistent with GetTargets. if (expr is IArrayIndexerExpression) ForEachPrefix(((IArrayIndexerExpression)expr).Target, action); else if (expr is IAddressOutExpression) ForEachPrefix(((IAddressOutExpression)expr).Expression, action); else if (expr is IPropertyReferenceExpression) ForEachPrefix(((IPropertyReferenceExpression)expr).Target, action); else if (expr is IFieldReferenceExpression) { IExpression target = ((IFieldReferenceExpression)expr).Target; if (!(target is IThisReferenceExpression)) ForEachPrefix(target, action); } else if (expr is ICastExpression) ForEachPrefix(((ICastExpression)expr).Expression, action); else if (expr is IPropertyIndexerExpression) ForEachPrefix(((IPropertyIndexerExpression)expr).Target, action); else if (expr is IEventReferenceExpression) ForEachPrefix(((IEventReferenceExpression)expr).Target, action); else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action); else if (expr is IMethodInvokeExpression) ForEachPrefix(((IMethodInvokeExpression)expr).Method, action); else if (expr is IMethodReferenceExpression) ForEachPrefix(((IMethodReferenceExpression)expr).Target, action); else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action); else if (expr is IDelegateInvokeExpression) ForEachPrefix(((IDelegateInvokeExpression)expr).Target, action); action(expr); } 

找到了吗 我们正在检查!

PVS-Studio警告
  • V3003检测到使用'if(A){...} else if(A){...}'模式。 存在逻辑错误的可能性。 检查行:1719、1727。编译器CodeRecognizer.cs 1719
  • V3003检测到使用'if(A){...} else if(A){...}'模式。 存在逻辑错误的可能性。 检查行:1721,1729。编译器CodeRecognizer.cs 1721

稍微简化代码以使问题更加明显。

 private void ForEachPrefix(IExpression expr, Action<IExpression> action) { if (....) .... else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action); .... else if (expr is IUnaryExpression) ForEachPrefix(((IUnaryExpression)expr).Expression, action); else if (expr is IAddressReferenceExpression) ForEachPrefix(((IAddressReferenceExpression)expr).Expression, action) .... } 

条件表达式, 然后复制多个if语句的分支。 也许这段代码是使用copy-paste方法编写的,这就是出现问题的原因。 现在事实证明,重复的then分支将永远不会执行,因为:

  • 如果条件表达式为真,则执行相应对中的第一个if语句的主体;
  • 如果条件表达式在第一种情况下为false,则在第二种情况下为false。

由于那么分支包含相同的动作,因此现在看起来像是令人困惑的冗余代码。 这可能是另一种问题-应该执行其他检查,而不是重复检查。

我们继续。

 public int Compare(Pair<int, int> x, Pair<int, int> y) { if (x.First < y.First) { if (x.Second >= y.Second) { // y strictly contains x return 1; } else { // No containment - order by left bound return 1; } } else if (x.First > y.First) { if (x.Second <= y.Second) { // x strictly contains y return -1; } else { // No containment - order by left bound return -1; } } .... } 

PVS-Studio警告
  • V3004'then '语句等效于'else'语句。 运行时RegexpTreeBuilder.cs 1080
  • V3004'then '语句等效于'else'语句。 运行时RegexpTreeBuilder.cs 1093

该代码看起来非常可疑,因为它包含两个条件语句,它们具有相同的thenelse分支主体。 在这两种情况下,可能值得返回不同的值。 或者,如果这是一种预期的行为,则删除多余的条件语句将很有用。

有有趣的周期。 下面的例子:

 private static Set<StochasticityPattern> IntersectPatterns(IEnumerable<StochasticityPattern> patterns) { Set<StochasticityPattern> result = new Set<StochasticityPattern>(); result.AddRange(patterns); bool changed; do { int count = result.Count; AddIntersections(result); changed = (result.Count != count); break; } while (changed); return result; } 

PVS-Studio警告V3020循环内无条件的“中断”。 编译器DefaultFactorManager.cs 474

由于无条件的break语句,仅执行了循环的一次迭代,并且甚至没有使用更改的控制变量。 通常,该代码看起来很奇怪和可疑。

在另一个类中发现了相同的方法(完全相同的副本)。 相应的分析仪警告: V3020循环内无条件的“中断”。 Visualizers.Windows FactorManagerView.cs 350

顺便说一句,一种方法在循环中遇到了无条件的continue语句(分析器使用相同的诊断程序发现了该方法),但是上面有一条注释,确认这是一个特殊的临时解决方案:

 // TEMPORARY continue; 

我记得在无条件中断声明附近没有这样的评论。

让我们继续前进。

 internal static DependencyInformation GetDependencyInfo(....) { .... IExpression resultIndex = null; .... if (resultIndex != null) { if (parameter.IsDefined( typeof(SkipIfMatchingIndexIsUniformAttribute), false)) { if (resultIndex == null) throw new InferCompilerException( parameter.Name + " has SkipIfMatchingIndexIsUniformAttribute but " + StringUtil.MethodNameToString(method) + " has no resultIndex parameter"); .... } .... } .... } 

PVS-Studio 警告V3022表达式'resultIndex == null'始终为false。 编译器FactorManager.cs 382

立即,我注意到在声明和上面的验证之间, resultIndex变量的值可以更改。 但是,在两次检查之间, resultIndex!= NullresultIndex == null,该值无法更改。 因此,表达式resultIndex == null的结果将始终为false ,这意味着永远不会引发异常。

希望您有兴趣自己发现错误,没有我的建议,找到问题,但是为了以防万一,我建议您再做一次。 方法代码很小,我将完整介绍它。

 public static Tuple<int, string> ComputeMovieGenre(int offset, string feature) { string[] genres = feature.Split('|'); if (genres.Length < 1 && genres.Length > 3) { throw new ArgumentException(string.Format( "Movies should have between 1 and 3 genres; given {0}.", genres.Length)); } double value = 1.0 / genres.Length; var result = new StringBuilder( string.Format( "{0}:{1}", offset + MovieGenreBuckets[genres[0]], value)); for (int i = 1; i < genres.Length; ++i) { result.Append( string.Format( "|{0}:{1}", offset + MovieGenreBuckets[genres[i].Trim()], value)); } return new Tuple<int, string>(MovieGenreBucketCount, result.ToString()); } 

让我们看看这里发生了什么。 输入字符串由字符“ |”解析。 如果数组的长度不符合预期,则必须引发异常。 等待第二个流派 ... Length <1 && genres.Length> 3吗? 由于没有数字立即落入表达式[[int.MinValue..1)(3..int.MaxValue]所需的值的范围内,所以表达式的结果将始终为false 。 因此,此检查不能防止任何事情,并且不会引发预期的异常。

这正是分析仪所警告的: V3022表达式'genres.Length <1 && genres.Length> 3'始终为假。 可能是“ ||” 这里应该使用运算符。 评估器Features.cs 242

遇到可疑裂变术。

 public static void CreateTrueThetaAndPhi(....) { .... double expectedRepeatOfTopicInDoc = averageDocLength / numUniqueTopicsPerDoc; .... int cnt = Poisson.Sample(expectedRepeatOfTopicInDoc); .... } 

PVS-Studio警告V3041该表达式从'int'类型隐式转换为'double'类型。 考虑使用显式类型转换以避免丢失小数部分。 例如:double A =(double)(X)/ Y;。 LDA Utilities.cs 74

这是可疑的:执行了整数除法(变量averageDocLengthnumUniqueTopicsPerDoc的类型为int ),并将结果写入double类型的变量。 问题是:这是专门完成的,还是仍然暗示着实数除法? 如果ExpectedRepeatOfTopicInDoc变量的类型为int ,这将清除可能的问题。

在其他地方,例如,使用Poisson.Sample方法(其参数是可疑变量ExpectedRepeatOfTopicInDoc) ,如下所述。

 int numUniqueWordsPerTopic = Poisson.Sample((double)averageWordsPerTopic); 

averageWordsPerTopic的类型为int ,已在使用位置转换为double

这是另一个使用地点:

 double expectedRepeatOfWordInTopic = ((double)numDocs) * averageDocLength / numUniqueWordsPerTopic; .... int cnt = Poisson.Sample(expectedRepeatOfWordInTopic); 

请注意,变量的名称与原始示例中的名称相同,仅使用实数除法来初始化ExpectedRepeatOfWordInTopic (由于numDocs显式转换为double )。

通常,值得一提的是分析仪发出警告的起始位置。

但是,对于是否值得编辑以及如何编辑的思考,让代码的作者(他们更好地了解),但是让我们走得更远。 到下一个可疑师。

 public static NonconjugateGaussian BAverageLogarithm(....) { .... double v_opt = 2 / 3 * (Math.Log(mx * mz / Ex2 / 2) - m); if (v_opt != v) { .... } .... } 

PVS-Studio警告V3041该表达式从'int'类型隐式转换为'double'类型。 考虑使用显式类型转换以避免丢失小数部分。 例如:double A =(double)(X)/ Y;。 运行时ProductExp.cs 137

分析仪再次检测到整数除法的可疑运算,如下 23是整数数字文字,并且表达式2/3的结果将为0 。 结果,整个表达式采用以下形式:

 double v_opt = 0 * expr; 

同意,有点奇怪。 我几次返回到此警告,试图找到某种捕获,而不是尝试将其添加到文章中。 该方法充满了数学和各种公式(坦率地说,我并不想分解),但您永远不知道会发生什么。 此外,我尝试对本文中写出的警告尽可能地表示怀疑,并且只有在对警告进行了更好的研究之后,我才对它们进行描述。

但是后来我想到了-为什么我需要因子0 ,写为2/3 ? 所以这个地方还是值得一看的。

 public static void WriteAttribute(TextWriter writer, string name, object defaultValue, object value, Func<object, string> converter = null) { if ( defaultValue == null && value == null || value.Equals(defaultValue)) { return; } string stringValue = converter == null ? value.ToString() : converter(value); writer.Write($"{name}=\"{stringValue}\" "); } 

PVS-Studio警告V3080可能取消空引用。 考虑检查“价值”。 编译器WriteHelpers.cs 78

根据条件对分析器进行相当合理的断言。 空引用的解引用可以在表达式value中发生。如果value == null,等于 (defaultValue) 。 由于此表达式是||运算符的右操作数,因此要计算它,左操作数必须为false ,为此,至少一个defaultValue \ value变量不为null就足够了。 结果,如果defaultValue!= Null ,并且value == null

  • defaultValue == null- > false ;
  • defaultValue == null && value == null- > false ; (未进行价值检查)
  • value.Equals(defaultValue) -> NullReferenceException ,因为valuenull

让我们看一个类似的情况:

 public FeatureParameterDistribution( GaussianMatrix traitFeatureWeightDistribution, GaussianArray biasFeatureWeightDistribution) { Debug.Assert( (traitFeatureWeightDistribution == null && biasFeatureWeightDistribution == null) || traitFeatureWeightDistribution.All( w => w != null && w.Count == biasFeatureWeightDistribution.Count), "The provided distributions should be valid and consistent in the number of features."); .... } 

PVS-Studio警告V3080可能取消空引用。 考虑检查“ traitFeatureWeightDistribution”。 推荐FeatureParameterDistribution.cs 65

我们排除了多余的部分,仅保留了用于计算布尔值的逻辑,因此更容易找出:

 (traitFeatureWeightDistribution == null && biasFeatureWeightDistribution == null) || traitFeatureWeightDistribution.All( w => w != null && w.Count == biasFeatureWeightDistribution.Count) 

同样,||的右操作数 仅当左计算的结果为false时才进行计算。 左操作数可以为false ,包括traitFeatureWeightDistribution == nullbiasFeatureWeightDistribution!= Null时 。 然后,将计算||运算符的右操作数,并调用traitFeatureWeightDistribution.All引发 ArgumentNullException

另一段有趣的代码:

 public static double GetQuantile(double probability, double[] quantiles) { .... int n = quantiles.Length; if (quantiles == null) throw new ArgumentNullException(nameof(quantiles)); if (n == 0) throw new ArgumentException("quantiles array is empty", nameof(quantiles)); .... } 

PVS-Studio警告V3095在验证是否为null之前,已使用“分位数”对象。 检查行:91、92。Runtime OuterQuantiles.cs 91

请注意, 首先访问quantiles.Length属性,然后检查了分位数是否 。 结果,如果分位数== null ,则该方法将引发异常,只是有一点点错误,而在不需要它的地方会抛出异常。 显然,他们弄乱了地方的线条。

如果您自己已经成功地解决了早期错误的检测问题,建议您冲泡一杯咖啡并尝试重复进行此操作,以下面的方法查找错误。 为了使它更有趣,我引用了该方法的全部代码。

链接到完整尺寸

图片2



好吧,好吧,那是个玩笑(或者你成功了吗?!)。 让我们简化一下任务:

 if (sample.Precision < 0) { precisionIsBetween = true; lowerBound = -1.0 / v; upperBound = -mean.Precision; } else if (sample.Precision < -mean.Precision) { precisionIsBetween = true; lowerBound = 0; upperBound = -mean.Precision; } else { // in this case, the precision should NOT be in this interval. precisionIsBetween = false; lowerBound = -mean.Precision; lowerBound = -1.0 / v; } 

变得更好了吗? 分析仪对此代码发出以下警告: V3008 “ lowerBound”变量已连续两次分配值。 也许这是一个错误。 检查线:324,323。运行时GaussianOp.cs 324

实际上,在最后一个else分支中, lowerBound变量的值连续两次被分配。 显然(根据上面的代码判断),变量upperBound必须包含在其中一个分配中。

我们会进一步。

 private void WriteAucMatrix(....) { .... for (int c = 0; c < classLabelCount; c++) { int labelWidth = labels[c].Length; columnWidths[c + 1] = labelWidth > MaxLabelWidth ? MaxLabelWidth : labelWidth; for (int r = 0; r < classLabelCount; r++) { int countWidth = MaxValueWidth; if (countWidth > columnWidths[c + 1]) { columnWidths[c + 1] = countWidth; } } .... } 

PVS-Studio警告V3081在嵌套循环内未使用“ r”计数器。 考虑检查“ c”计数器的用法。 命令行ClassifierEvaluationModule.cs 459

请注意,在此循环的主体中未使用内部循环的计数器-r-。 因此,事实证明,在内循环的所有迭代期间,对相同元素执行相同的操作-因为索引还使用外循环的计数器( c ),而不使用内循环的计数器( r )。

让我们看看还有什么有趣的地方。

 public RegexpFormattingSettings( bool putOptionalInSquareBrackets, bool showAnyElementAsQuestionMark, bool ignoreElementDistributionDetails, int truncationLength, bool escapeCharacters, bool useLazyQuantifier) { this.PutOptionalInSquareBrackets = putOptionalInSquareBrackets; this.ShowAnyElementAsQuestionMark = showAnyElementAsQuestionMark; this.IgnoreElementDistributionDetails = ignoreElementDistributionDetails; this.TruncationLength = truncationLength; this.EscapeCharacters = escapeCharacters; } 

PVS-Studio警告V3117未使用构造函数参数'useLazyQuantifier'。 运行时RegexpFormattingSettings.cs 38

构造函数不使用一个参数useLazyQuantifier 。 对于在类UseLazyQuantifier中定义了具有相应名称和类型的属性这一事实的背景下,这尤其可疑。 显然,他们忘记了通过相应的参数对其进行初始化。

遇到了几个潜在的危险事件处理程序。 下面是其中之一的示例:

 public class RecommenderRun { .... public event EventHandler Started; .... public void Execute() { // Report that the run has been started if (this.Started != null) { this.Started(this, EventArgs.Empty); } .... } .... } 

PVS-Studio警告V3083对事件'Started'的不安全调用,可能会发生NullReferenceException。 请考虑在调用事件之前将事件分配给局部变量。 评估程序RecommenderRun.cs 115

事实是,在检查null不等式和调用处理程序之间,可以取消预订事件,并且如果在检查null和处理程序调用之间该事件尚无订阅者,则将抛出NullReferenceException 。 例如,要避免此类问题,您可以将指向委托链的链接保存在局部变量中,或使用“?”运算符。 调用处理程序。

除了上面的代码片段外,还有35个这样的地方。

顺便说一下,还满足了V3024的 785条警告。 当使用运算符'!='或'=='比较实数时,会发出警告V3024 。 我不会在这里详细说明为什么这种比较并不总是正确的-有关此问题的更多信息写在文档中,还有指向StackOverflow的链接(就是这样)。

考虑到经常满足公式和计算的要求,这些警告也很重要,尽管它们被带到了第3级(因为它们在所有项目中都相去甚远)。

如果您确定这些警告无关紧要,则只需单击一下即可将其删除,从而减少了分析仪操作的总数。



结论


不知何故,我很长一段时间都没有写关于检查项目的文章了,再次触摸这个过程是很愉快的。 希望您也从本文中学到了一些新的或有用的知识,或者至少有兴趣地阅读了一下。

我希望开发人员尽早纠正问题区域,并提醒您犯错误是正常现象,但我们是人。 为此,需要使用诸如静态分析仪之类的其他工具来查找某人错过的东西,对吗? 无论如何-祝项目进展顺利,并感谢您的工作!

请记住,静态分析仪的最大好处是可以正常使用

祝一切顺利!



如果您想与讲英语的读者分享这篇文章,请使用以下链接:Sergey Vasiliev。 Infer.NET代码中存在什么错误?

Source: https://habr.com/ru/post/zh-CN429578/


All Articles