Estudo de qualidade de código Microsoft Open XML SDK

image1.png


Meu conhecimento do Open XML SDK começou quando eu precisava de uma biblioteca para criar documentos do Word com alguns relatórios. Depois de trabalhar com a API do Word por mais de 7 anos, eu queria tentar algo novo e mais conveniente. Foi assim que descobri que a Microsoft tem uma solução alternativa. Tradicionalmente, nós pré-verificamos programas e bibliotecas usados ​​na empresa usando o analisador PVS-Studio.



Introdução



Office Open XML, também conhecido como OpenXML ou OOXML, é um formato baseado em XML para documentos de escritório, incluindo documentos de processamento de texto, planilhas, apresentações e diagramas, formas e outros gráficos. A especificação foi desenvolvida pela Microsoft e adotada pela ECMA International em 2006. Em junho de 2014, a Microsoft lançou o Open XML SDK em código aberto. A fonte agora está disponível no GitHub sob a licença do MIT.



Para encontrar erros no código-fonte da biblioteca, usamos PVS-Studio . É uma ferramenta para detectar bugs e vulnerabilidades potenciais no código-fonte de programas escritos em C, C ++, C # e Java. Funciona em sistemas de 64 bits no Windows, Linux e macOS.



O projeto é pequeno o suficiente e houve poucos avisos. Mas a escolha da imagem do título foi baseada justamente nos resultados. Existem muitos operadores condicionais inúteis no código. Parece-me que se você refatorar todos esses lugares no código, o volume será visivelmente reduzido. Como resultado, a legibilidade do código também aumentará.



Por que a API do Word e não o Open XML SDK



Como você deve ter adivinhado pelo título, continuei a usar a API do Word. Este método tem muitas desvantagens:



  • API desajeitada antiga;
  • O Microsoft Office deve estar instalado;
  • A necessidade de distribuir um kit de distribuição com bibliotecas Office;
  • Dependência da API do Word nas configurações de localidade do sistema;
  • Baixa velocidade de trabalho.


Com o local em geral, aconteceu um incidente divertido. O Windows tem uma dúzia de configurações regionais. Em um dos computadores servidores, havia uma confusão com as localidades dos EUA e do Reino Unido. Por causa disso, foram criados documentos do Word, onde em vez do símbolo do dólar havia um rublo e as libras não eram exibidas. Melhorar as configurações do sistema operacional corrigiu o problema.



Listando tudo isso, me perguntei de novo por que ainda a uso ...



Mas não, ainda gosto mais da API do Word e aqui está o porquê.



OOXML se parece com isto:



<?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>


Onde <w: r> (Word Run) não é uma frase, nem mesmo uma palavra, mas qualquer pedaço de texto que tenha atributos diferentes dos fragmentos de texto adjacentes.



É programado com algo assim:



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


O documento possui uma estrutura interna específica, e no código você precisa criar os mesmos elementos. O Open XML SDK, em minha opinião, não tem camada de acesso a dados abstratos suficiente. A criação de um documento usando a API do Word será mais clara e curta. Especialmente quando se trata de tabelas e outras estruturas de dados complexas.



Por sua vez, o Open XML SDK resolve uma ampla gama de tarefas. Com ele, você pode criar documentos não só para Word, mas também para Excel e PowerPoint. Esta biblioteca provavelmente é mais adequada para algumas tarefas, mas decidi ficar na API do Word por enquanto. Em qualquer caso, não será possível abandoná-lo completamente, porque para necessidades internas, estamos desenvolvendo um plugin para o Word, onde é possível utilizar apenas a API do Word.



Dois valores para string



V3008 A variável '_rawOuterXml' recebe valores duas vezes sucessivamente. Talvez seja um engano. Verifique as linhas: 164, 161. OpenXmlElement.cs 164



internal string RawOuterXml
{
    get => _rawOuterXml;

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

        _rawOuterXml = value;
    }
}


O tipo de string pode ter 2 tipos de valores: valor nulo e valor de texto. Usar o significado textual é definitivamente mais seguro, mas ambas as abordagens são válidas. Neste projeto , é inaceitável usar o valor nulo e é reescrito para string.Empty ... pelo menos é assim que se pretendia. Porém, devido a um erro em RawOuterXml , você ainda pode escrever null e, em seguida, referir-se a este campo, recebendo uma NullReferenceException .



A expressão V3022 'namespaceUri! = Null' é sempre verdadeira. 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)  // <=
        {
            ....
        }
        ....
    }
    ....
}


Este trecho usa a mesma abordagem, o autor do código não cometeu nenhum erro grave, mas ainda cheira a uma refatoração falhada. Provavelmente, uma verificação pode ser removida aqui, o que reduzirá a largura do código e, portanto, aumentará sua legibilidade.



Sobre a compactação do código



image2.png


V3009 É estranho que este método sempre retorne um e o mesmo valor de '".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";
    }
}


Não sei se há algum erro de digitação aqui, ou se o autor do código escreveu um código "legal" em sua opinião. Tenho certeza de que não faz sentido retornar tantos valores do mesmo tipo de uma função, e o código pode ser bastante reduzido.



Este não é o único lugar. Aqui estão mais alguns desses avisos:



  • V3009 É estranho que este método sempre retorne um e o mesmo valor de '".xml"'. CustomPropertyPartTypeInfo.cs 25
  • V3009 É estranho que este método sempre retorne um e o mesmo valor de '".bin"'. EmbeddedControlPersistenceBinaryDataPartTypeInfo.cs 22


Seria interessante ouvir por que escrever assim.



V3139 Duas ou mais ramificações de caso executam as mesmas ações. 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;
        ....
    }
    ....
}


Existem menos perguntas sobre este código. Muito provavelmente, casos idênticos podem ser combinados e o código se tornará mais curto e mais óbvio.



Mais alguns desses lugares:



  • V3139 Duas ou mais ramificações de caso executam as mesmas ações. OpenXmlMiscNode.cs 312
  • V3139 Duas ou mais ramificações de caso executam as mesmas ações. CustomPropertyPartTypeInfo.cs 30
  • V3139 Duas ou mais ramificações de caso executam as mesmas ações. CustomXmlPartTypeInfo.cs 15
  • V3139 Duas ou mais ramificações de caso executam as mesmas ações. OpenXmlElement.cs 1803


Aqueles sempre verdadeiros / falsos



Agora é a hora de fornecer alguns exemplos de código que determinaram minha escolha da imagem do título.



Aviso 1



A expressão V3022 'Complete ()' é sempre falsa. ParticleCollection.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;
}


A propriedade IsComplete é usada 2 vezes e é fácil entender pelo código que seu valor não mudará. Assim, no final da função, você pode simplesmente retornar o segundo valor do operador ternário - verdadeiro .



Aviso 2



A expressão V3022 '_elementStack.Count> 0' é sempre verdadeira. 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;
    }
    ....
}


Obviamente, se não houver 0 elementos em _elementStack , haverá mais deles. O código pode ser reduzido em pelo menos 8 linhas.



Aviso 3



A expressão V3022 'rootElement == null' é sempre falsa. 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(....);
  }
  ....
}


A função CreateElement não pode retornar nulo . Se a empresa fez uma regra para escrever métodos para criar xml-nodes que retornam um objeto válido ou lançam uma exceção, os usuários de tais funções não precisam abusar de verificações adicionais.



Aviso 4



A expressão V3022 'nameProvider' sempre não é nula. O operador '?.' é excessivo. 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();
}


O operador is tem o seguinte padrão:



expr is type varname


Se expr for avaliado como true , então o objeto varname será válido e não precisa ser comparado com null novamente , como é feito neste trecho de código.



Aviso 5



V3022 Expression 'extension == ".xlsx" || extension == ".xlsm" 'é sempre falso. 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;
        }
        ....
    }
    ....
}


Um código interessante foi descoberto. Primeiro autor eliminar todos os documentos com as seguintes extensões não são .pptx , .pptm ,. potx e. potm e, em seguida, decidiu verificar se não havia .xlsx e .xlsm entre eles . A função PresentationDocument é definitivamente uma vítima de refatoração.



Aviso 7



A expressão V3022 'OpenSettings.MarkupCompatibilityProcessSettings == null' é sempre falsa. 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;
        }
    }
}


A propriedade MarkupCompatibilityProcessSettings nunca retorna nulo . Se no getter o campo da classe for nulo , o objeto será sobrescrito por um novo. Além disso, observe que esta não é uma chamada recursiva para uma propriedade, mas propriedades com o mesmo nome de classes diferentes. Talvez alguma confusão tenha levado à redação de cheques desnecessários.



Outros avisos



Aviso 1



V3080 Possível desreferência nula. Considere inspecionar '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;    // <=
    ....
}


E aqui está um exemplo em que a verificação adicional não é suficiente. O método PreviousSibling pode retornar nulo e o resultado desta função é usado imediatamente sem verificação.



Mais 2 lugares perigosos:



  • V3080 Possível desreferência nula. Considere inspecionar 'prevNode'. OpenXmlCompositeElement.cs 489
  • V3080 Possível desreferência nula. Considere inspecionar 'prevNode'. OpenXmlCompositeElement.cs 497


Aviso 2



V3093 O operador '&' avalia ambos os operandos. Talvez um operador de curto-circuito '&&' deva ser usado em seu lugar. 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;
            }
        }
    }
    ....
}


Algumas pessoas gostam de usar o operador '&' em expressões lógicas onde não deveriam. No caso desse operador, o segundo operando é avaliado primeiro, independentemente do resultado do primeiro. Este não é um erro muito sério aqui, mas esse código desleixado após a refatoração pode levar a possíveis exceções NullReferenceException .



Aviso 3



V3097 Possível exceção: o tipo marcado por [Serializable] contém membros não serializáveis ​​não marcados por [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; } // <=
}


A serialização da classe OpenXmlPackageValidationEventArgs pode falhar porque uma das propriedades foi esquecida de ser marcada como não serializável. Ou você precisa modificar o tipo de retorno desta propriedade para ser serializável, caso contrário, uma exceção pode ocorrer em tempo de execução.



Conclusão



Nós, na empresa, amamos os projetos e tecnologias da Microsoft. Na seção onde listamos os projetos Open Source testados com PVS-Studio, até alocamos uma seção separada para a Microsoft. Já são 21 projetos acumulados, sobre os quais já foram escritos 26 artigos. Este é o dia 27.



Tenho certeza de que você está se perguntando se existe a Microsoft entre nossos clientes. A resposta é sim! Mas não esqueçamos que esta é uma grande corporação líder em desenvolvimento em todo o mundo. Definitivamente existem divisões que já usam o PVS-Studio em seus projetos, mas há ainda mais daquelas que não usam! E nossa experiência com projetos de código aberto mostra que eles claramente carecem de uma boa ferramenta para encontrar bugs;).





Mais notícias da notícia para os interessados ​​em analisar código em C ++, C # e Java: recentemente adicionamos suporte para o padrão OWASP e estamos aumentando ativamente sua cobertura.





Se você deseja compartilhar este artigo com um público que fala inglês, use o link de tradução: Svyatoslav Razmyslov. Analisando a qualidade do código do SDK de XML aberto da Microsoft .



All Articles