Microsoft Open XML SDK代码质量研究

image1.png


当我需要一个用于创建带有一些报告的Word文档的库时,我就开始接触Open XML SDK。在使用Word API超过7年之后,我想尝试一些新颖且更方便的方法。这就是我发现Microsoft有替代解决方案的方式。按照传统,我们使用PVS-Studio分析仪预先检查公司中使用的程序和库。



介绍



Office Open XML,也称为OpenXML或OOXML,是用于办公室文档的基于XML的格式,包括文字处理文档,电子表格,演示文稿以及图表,形状和其他图形。该规范由Microsoft开发,并于2006年被ECMA International采用。 2014年6月,Microsoft以开源形式发布了Open XML SDK。现在,该源代码已在MIT许可下GitHub上提供



为了找到库源代码中的错误,我们使用了PVS-Studio。它是用于识别用C,C ++,C#和Java编写的程序的源代码中的错误和潜在漏洞的工具。在Windows,Linux和macOS上的64位系统上运行。



该项目足够小,几乎没有警告。但是标题图片的选择正是基于结果。代码中有很多无用的条件运算符。在我看来,如果您重构代码中的所有此类位置,那么体积将明显减少。结果,代码的可读性也将提高。



为什么使用Word API而不使用Open XML SDK



正如您可能从标题中猜到的那样,我继续使用Word API。此方法有很多缺点:



  • 旧的笨拙的API;
  • 必须安装Microsoft Office;
  • 需要分发带有Office库的分发工具包;
  • Word API对系统区域设置的依赖性;
  • 工作速度低。


总体而言,发生了一件有趣的事件。Windows有十几个区域设置。在其中一台服务器计算机上,有来自美国和英国语言环境的乱七八糟的东西。因此,创建了Word文档,在其中用卢布代替美元符号,而根本不显示英镑。改进操作系统设置可以解决该问题。



列出所有这些,我再次想知道为什么我仍然使用它...



但是,不,我仍然更喜欢Word API,这就是原因。



OOXML看起来像这样:



<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<w:document ....>
  <w:body>
    <w:p w:rsidR="00E22EB6"
         w:rsidRDefault="00E22EB6">
      <w:r>
        <w:t>This is a paragraph.</w:t>
      </w:r>
    </w:p>
    <w:p w:rsidR="00E22EB6"
         w:rsidRDefault="00E22EB6">
      <w:r>
        <w:t>This is another paragraph.</w:t>
      </w:r>
    </w:p>
  </w:body>
</w:document>


其中<w:r>(单词运行)不是句子,甚至不是单词,而是具有与相邻文本片段不同的属性的任何文本。



它的编程如下:



Paragraph para = body.AppendChild(new Paragraph());
Run run = para.AppendChild(new Run());
run.AppendChild(new Text(txt));


该文档具有特定的内部结构,并且需要在代码中创建相同的元素。在我看来,Open XML SDK没有足够的抽象数据访问层。使用Word API创建文档将更加清晰和简短。特别是涉及表格和其他复杂数据结构时。



反过来,Open XML SDK解决了许多任务。使用它,您不仅可以为Word创建文档,还可以为Excel和PowerPoint创建文档。该库可能更适合于某些任务,但我决定暂时保留Word API。在任何情况下,都不可能完全放弃它,因为 为了满足内部需求,我们正在开发Word插件,并且可以仅使用Word API。



字符串的两个值



V3008'_rawOuterXml '变量被连续分配两次值。也许这是一个错误。检查行:164,161。OpenXmlElement.cs 164



internal string RawOuterXml
{
    get => _rawOuterXml;

    set
    {
        if (string.IsNullOrEmpty(value))
        {
            _rawOuterXml = string.Empty;
        }

        _rawOuterXml = value;
    }
}


字符串 类型可以有两种类型的值:null和文本值。使用文本含义绝对更安全,但是两种方法都是有效的。在此项目中,使用null是不可接受的,并且将其重写为string.Empty ...至少是这样做的目的。但是由于RawOuterXml中的错误,您仍然可以编写null,然后引用此字段,从而收到NullReferenceException



V3022表达式'namespaceUri!= Null'始终为true。 OpenXmlElement.cs 497



public OpenXmlAttribute GetAttribute(string localName, string namespaceUri)
{
    ....
    if (namespaceUri == null)
    {
        // treat null string as empty.
        namespaceUri = string.Empty;
    }
    ....
    if (HasAttributes)
    {
        if (namespaceUri != null)  // <=
        {
            ....
        }
        ....
    }
    ....
}


此代码段使用相同的方法,代码的编写者没有犯任何重大错误,但是仍然闻到不良的重构。最有可能的是,可以在此处删除一项检查,这将减少代码的宽度,并因此增加其可读性。



关于代码的紧凑性



image2.png


V3009奇怪的是,此方法总是返回一个相同的值“ .xml”。CustomXmlPartTypeInfo.cs 31



internal static string GetTargetExtension(CustomXmlPartType partType)
{
    switch (partType)
    {
        case CustomXmlPartType.AdditionalCharacteristics:
            return ".xml";

        case CustomXmlPartType.Bibliography:
            return ".xml";

        case CustomXmlPartType.CustomXml:
            return ".xml";

        case CustomXmlPartType.InkContent:
            return ".xml";

        default:
            return ".xml";
    }
}


我不知道这里是否有错字,或者代码的作者是否认为自己写的是“好”的代码。我确信从一个函数中返回这么多相同类型的值是没有意义的,并且代码可以大大减少。



这不是唯一的地方。以下是其中一些警告:



  • V3009奇怪的是,此方法始终返回一个相同的值“ .xml”。CustomPropertyPartTypeInfo.cs 25
  • V3009奇怪的是,此方法始终返回一个相同的值“ .bin”。EmbeddedControlPersistenceBinaryDataPartTypeInfo.cs 22


听到为什么这样写会很有趣。



V3139两个或更多案例分支执行相同的操作。OpenXmlPartReader.cs 560



private void InnerSkip()
{
    Debug.Assert(_xmlReader != null);

    switch (_elementState)
    {
        case ElementState.Null:
            ThrowIfNull();
            break;

        case ElementState.EOF:
            return;

        case ElementState.Start:
            _xmlReader.Skip();
            _elementStack.Pop();
            GetElementInformation();
            return;

        case ElementState.End:
        case ElementState.MiscNode:
            // cursor is end element, pop stack
            _xmlReader.Skip();
            _elementStack.Pop();
            GetElementInformation();
            return;
        ....
    }
    ....
}


关于此代码的问题较少。很有可能,相同的情况可以合并,并且代码将变得更短,更明显。



还有一些这样的地方:



  • V3139两个或更多分支机构执行相同的操作。OpenXmlMiscNode.cs 312
  • V3139两个或更多案例分支执行相同的操作。CustomPropertyPartTypeInfo.cs 30
  • V3139两个或更多分支机构执行相同的操作。CustomXmlPartTypeInfo.cs 15
  • V3139两个或更多分支机构执行相同的操作。OpenXmlElement.cs 1803


那些总是对/错



现在该提供一些确定我选择标题图像的代码示例了。



警告1



V3022表达式'Complete()'始终为false。粒子收集.cs 243



private bool IsComplete => Current is null ||
                           Current == _collection._element.FirstChild;

public bool MoveNext()
{
    ....
    if (IsComplete)
    {
        return Complete();
    }

    if (....)
    {
        return Complete();
    }

    return IsComplete ? Complete() : true;
}


IsComplete 属性使用了2次,从代码中很容易理解它的值不会改变。因此,在函数的末尾,你可以简单地返回三元运算符的第二个值-真实



警告2



V3022表达式'_elementStack.Count> 0'始终为true。OpenXmlDomReader.cs 501



private readonly Stack<OpenXmlElement> _elementStack;

private bool MoveToNextSibling()
{
    ....
    if (_elementStack.Count == 0)
    {
        _elementState = ElementState.EOF;
        return false;
    }
    ....
    if (_elementStack.Count > 0) // <=
    {
        _elementState = ElementState.End;
    }
    else
    {
        // no more element, EOF
        _elementState = ElementState.EOF;
    }
    ....
}


显然,如果_elementStack中不存在0个元素,则它们将更多。该代码至少可以缩短8行。



警告3



V3022表达式'rootElement == null'始终为false。OpenXmlPartReader.cs 746



private static OpenXmlElement CreateElement(string namespaceUri, string name)
{
    if (string.IsNullOrEmpty(name))
    {
        throw new ArgumentException(....);
    }

    if (NamespaceIdMap.TryGetNamespaceId(namespaceUri, out byte nsId)
        && ElementLookup.Parts.Create(nsId, name) is OpenXmlElement element)
    {
        return element;
    }

    return new OpenXmlUnknownElement();
}

private bool ReadRoot()
{
  ....
  var rootElement = CreateElement(....);

  if (rootElement == null) // <=
  {
      throw new InvalidDataException(....);
  }
  ....
}


CreateElement 函数不能返回null如果公司制定了规则来编写用于创建返回有效对象或引发异常的xml节点的方法,则此类函数的用户将无法滥用其他检查。



警告4



V3022表达式'nameProvider'始终不为null。运营商 '?。' 太过分了。OpenXmlSimpleTypeExtensions.cs 50



public static XmlQualifiedName GetSimpleTypeQualifiedName(....)
{
    foreach (var validator in validators)
    {
        if (validator is INameProvider nameProvider &&
            nameProvider?.QName is XmlQualifiedName qname) // <=
        {
            return qname;
        }
    }

    return type.GetSimpleTypeQualifiedName();
}


is运算符具有以下模式:



expr is type varname


如果expr评估为true,则varname对象将是有效的,不需要再次与null比较,如此代码段所示。



警告5



V3022表达式'extension ==“ .xlsx” || extension ==“ .xlsm”'始终为false。 PresentationDocument.cs 246



public static PresentationDocument CreateFromTemplate(string path)
{
    ....
    string extension = Path.GetExtension(path);
    if (extension != ".pptx" && extension != ".pptm" &&
        extension != ".potx" && extension != ".potm")
    {
        throw new ArgumentException("...." + path, nameof(path));
    }

    using (PresentationDocument template = PresentationDocument.Open(....)
    {
        PresentationDocument document = (PresentationDocument)template.Clone();

        if (extension == ".xlsx" || extension == ".xlsm")
        {
            return document;
        }
        ....
    }
    ....
}


结果是一个有趣的代码。第一作者清除了具有以下扩展名的所有文档,而不是.pptx.pptmPOTX和。potm,然后决定检查其中是否没有.xlsx.xlsmPresentationDocument函数绝对是重构的受害者。



警告7



V3022表达式'OpenSettings.MarkupCompatibilityProcessSettings == null'始终为false。 OpenXmlPackage.cs 661



public MarkupCompatibilityProcessSettings MarkupCompatibilityProcessSettings
{
    get
    {
        if (_mcSettings is null)
        {
            _mcSettings = new MarkupCompatibilityProcessSettings(....);
        }

        return _mcSettings;
    }

    set
    {
        _mcSettings = value;
    }
}

public MarkupCompatibilityProcessSettings MarkupCompatibilityProcessSettings
{
    get
    {
        if (OpenSettings.MarkupCompatibilityProcessSettings == null) // <=
        {
            return new MarkupCompatibilityProcessSettings(....);
        }
        else
        {
            return OpenSettings.MarkupCompatibilityProcessSettings;
        }
    }
}


MarkupCompatibilityProcessSettings 属性从不返回null如果在getter方法中发现class字段为null,则该对象将被新对象覆盖。另外,请注意,这不是对一个属性的递归调用,而是来自不同类的相同名称的属性。也许有些混乱导致编写不必要的支票。



其他警告



警告1



V3080可能会取消引用。考虑检查“ previousSibling”。的OpenXmlCompositeElement.cs 380



public OpenXmlElement PreviousSibling()
{
    if (!(Parent is OpenXmlCompositeElement parent))
    {
        return null;
    }
    ....
}

public override T InsertBefore<T>(T newChild, OpenXmlElement referenceChild)
{
    ....
    OpenXmlElement previousSibling = nextNode.PreviousSibling();
    prevNode.Next = nextNode;
    previousSibling.Next = prevNode;    // <=
    ....
}


这是一个示例,其中额外的验证仅仅是不够的。PreviousSibling方法可以返回null,并且无需检查即可立即使用此函数的结果。



2个更危险的地方:



  • V3080可能为空的取消引用。考虑检查“ prevNode”。OpenXmlCompositeElement.cs 489
  • V3080可能为空的取消引用。考虑检查“ prevNode”。OpenXmlCompositeElement.cs 497


警告2



V3093 '&'运算符同时计算两个操作数。也许应该使用短路的“ &&”运算符。UniqueAttributeValueConstraint.cs 60



public override ValidationErrorInfo ValidateCore(ValidationContext context)
{
    ....
    foreach (var e in root.Descendants(....))
    {
        if (e != element & e.GetType() == elementType) // <=
        {
            var eValue = e.ParsedState.Attributes[_attribute];

            if (eValue.HasValue && _comparer.Equals(....))
            {
                return true;
            }
        }
    }
    ....
}


有些人喜欢将'&'运算符应用于不应该使用的逻辑表达式。对于此运算符,无论第一个操作数的结果如何,都将首先计算第二个操作数。这不是一个很严重的错误,但是重构后的这种草率代码可能会导致潜在的NullReferenceException异常



警告3



V3097可能的例外:由[可序列化]标记的类型包含未由[NonSerialized]标记的不可序列化的成员。 OpenXmlPackageValidationEventArgs.cs 15



[Serializable]
[Obsolete(ObsoleteAttributeMessages.ObsoleteV1ValidationFunctionality, false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenXmlPackageValidationEventArgs : EventArgs
{
    private string _message;

    [NonSerialized]
    private readonly object _sender;

    [NonSerialized]
    private OpenXmlPart _subPart;

    [NonSerialized]
    private OpenXmlPart _part;

    ....

    internal DataPartReferenceRelationship
        DataPartReferenceRelationship { get; set; } // <=
}


由于忘记了其中一个属性被标记为不可序列 化,因此OpenXmlPackageValidationEventArgs类的序列化可能会失败。或者您需要将此属性的返回类型修改为可序列化,否则在运行时可能会发生异常。



结论



我们公司喜欢Microsoft的项目和技术。在列出使用PVS-Studio测试的开源项目的部分中,我们甚至为Microsoft分配了单独的部分。已经积累了21个项目,已撰写了26篇文章。这是27日。



我确定您想知道我们的客户中是否有Microsoft。答案是肯定的!但是,请不要忘记这是一家领导全球发展的大型公司。肯定有一些部门已在其项目中使用PVS-Studio,但还有更多部门未使用!而且我们在开源项目中的经验表明,它们显然缺乏查找错误的好工具;)。





对于那些有兴趣分析C ++,C#和Java代码的人来说,更多的新闻来自于新闻:我们最近增加了对OWASP标准的支持,并正在积极扩大其覆盖范围。





如果您想与讲英语的读者分享本文,请使用翻译链接:Svyatoslav Razmyslov。分析Microsoft Open XML SDK的代码质量



All Articles