
Quando decidimos procurar erros no projeto do SDK do Azure para .NET, ficamos agradavelmente surpresos com seu tamanho. "Três milhões e meio de linhas de código", continuávamos dizendo, estudando as estatísticas do projeto. Pode haver muitas descobertas. Ai e ai! O projeto acabou sendo astuto. Então, qual foi o entusiasmo do projeto e como ele foi verificado - leia este artigo.
Sobre o projeto
Estou escrevendo este artigo acompanhando o anterior, que também tratava de um projeto relacionado ao Microsoft Azure:
Azure PowerShell: Principalmente inofensivo . Então, desta vez eu estava apostando em um número sólido de erros diversos e interessantes. Afinal, o tamanho do projeto é um fator muito importante em termos de análise estática, principalmente ao verificar um projeto pela primeira vez. De fato, na prática, o aplicativo de verificação única não é a abordagem correta. No entanto, se os desenvolvedores fizerem isso, isso ocorrerá apenas no estágio de introdução do analisador. Ao mesmo tempo, ninguém se preocupa em separar o enorme número de avisos e apenas os classifica como dívida técnica usando mecanismos de supressão de avisos em massa e armazenando-os em bases especiais. Por falar nisso, ter um bom número de avisos é bom ao executar o analisador pela primeira vez. Quanto a nós, fazemos verificações únicas para fins de pesquisa. Por esse motivo, os grandes projetos são sempre mais preferíveis para a análise a seguir, em comparação com os pequenos.
No entanto, o projeto SDK do Azure para .NET imediatamente provou ser um ambiente de teste inviável. Mesmo seu tamanho impressionante não ajudou, mas foi complicado trabalhar nele. O motivo é dado nas seguintes estatísticas do projeto:
- arquivos de origem .cs (não incluindo testes): 16,500
- Soluções do Visual Studio (.sln): 163
- Linhas de código não vazias: 3 462 000
- Desses gerados automaticamente: cerca de 3.300.000
- O repositório do projeto está disponível no GitHub .
Aproximadamente 95% do código é gerado automaticamente e grande parte desse código é repetido várias vezes. A verificação de tais projetos com um analisador estático geralmente é demorada e inútil, pois há muito código viável, mas ilógico (pelo menos à primeira vista) e redundante. Isso leva a um grande número de falsos positivos.
Toda essa quantidade de código espalhada por mais de 163 soluções do Visual Studio se tornou a "cereja no topo". Foram necessários alguns esforços para verificar o código restante (não gerado automaticamente). O que realmente ajudou foi o fato de que todo o código gerado automaticamente foi armazenado em subdiretórios de soluções pelo caminho relativo "<Diretório de Soluções> \ src \ Gerado". Além disso, cada arquivo .cs desse tipo contém um comentário especial na tag
<auto-generated> :
Para a pureza do experimento, verifiquei remotamente cerca de dez soluções geradas automaticamente selecionadas aleatoriamente. Vou contar sobre o resultado mais tarde.
Portanto, apesar da pequena quantidade de código "honesto" restante, ainda consegui encontrar uma série de erros do que restava. Desta vez, não vou citar avisos na ordem dos códigos de diagnóstico do PVS-Studio. Em vez disso, agruparei as mensagens nas soluções em que foram encontradas.
Bem, vamos ver o que consegui encontrar no código do Azure SDK para .NET.
Microsoft.Azure.Management.Advisor
Essa é uma das muitas soluções que contém código gerado automaticamente. Como disse anteriormente, verifiquei aleatoriamente cerca de uma dúzia dessas soluções. Em cada caso, os avisos eram os mesmos e, como esperado, inúteis. Aqui estão alguns exemplos.
A expressão
V3022 'Credenciais! = Nulo' sempre é verdadeira. AdvisorManagementClient.cs 204
Obviamente, esse código é redundante e a verificação
Credentials! = Null é inútil. No entanto, o código funciona. E é gerado automaticamente. Por esse motivo, não há queixas aqui.
A expressão
V3022 '_queryParameters.Count> 0' sempre é falsa. ConfigurationsOperations.cs 871
Mais uma vez, parece uma construção ilógica. Por alguma razão, os autores do código verificam o tamanho da lista
vazia recém-criada. De fato, está tudo correto. Nesse momento, a verificação não faz sentido, mas, se os desenvolvedores adicionarem a geração de listas, por exemplo, com base em outra coleção, a verificação definitivamente valerá a pena. Mais uma vez - nenhuma reivindicação ao código, é claro, com relação à sua origem.
Centenas de avisos semelhantes foram emitidos para cada solução gerada automaticamente. Dada a sua futilidade, acho que não faz sentido discutir mais esses casos. Em seguida, apenas erros reais no código "normal" serão considerados.
Azure.Core
V3001 Existem
subexpressões idênticas 'buffer.Length' à esquerda e à direita do operador '<'. AzureBaseBuffersExtensions.cs 30
public static async Task WriteAsync(...., ReadOnlyMemory<byte> buffer, ....) { byte[]? array = null; .... if (array == null || buffer.Length < buffer.Length)
O erro na condição foi provavelmente o resultado de copiar e colar. De acordo com o fato de que o
buffer é copiado na
matriz , a verificação deve se parecer com:
if (array == null || array.Length < buffer.Length)
De qualquer forma, como sempre digo, o autor do código deve lidar com a correção desses erros.
V3083 Chamada não segura do evento '_onChange', NullReferenceException é possível. Considere atribuir um evento a uma variável local antes de invocá-lo. ClientOptionsMonitor.cs 44
private event Action<TOptions, string> _onChange; .... private void InvokeChanged(....) { .... if (_onChange != null) { _onChange.Invoke(options, name); } }
Não é crítico, mas um erro está aqui. O consumidor pode cancelar a assinatura do evento entre verificar se o evento é
nulo e sua chamada. Em seguida, a variável
_onChange será
nula e uma exceção será lançada. Este código deve ser reescrito de uma maneira mais segura. Por exemplo, da seguinte maneira:
private void InvokeChanged(....) { .... _onChange?.Invoke(options, name); }
Azure.Messaging.EventHubs
V3080 Possível desreferência nula. Considere inspecionar 'eventPropertyValue'. AmqpMessageConverter.cs 650
private static bool TryCreateEventPropertyForAmqpProperty( object amqpPropertyValue, out object eventPropertyValue) { eventPropertyValue = null; .... switch (GetTypeIdentifier(amqpPropertyValue)) { case AmqpProperty.Type.Byte: .... case AmqpProperty.Type.String: eventPropertyValue = amqpPropertyValue; return true; .... } .... switch (amqpPropertyValue) { case AmqpSymbol symbol: eventPropertyValue = ....; break; case byte[] array: eventPropertyValue = ....; break; case ArraySegment<byte> segment when segment.Count == segment.Array.Length: eventPropertyValue = ....; break; case ArraySegment<byte> segment: .... eventPropertyValue = ....; break; case DescribedType described when (described.Descriptor is AmqpSymbol): eventPropertyValue = ....; break; default: var exception = new SerializationException( string.Format(...., eventPropertyValue.GetType().FullName));
Vamos ver o que acontece com o valor da variável
eventPropertyValue no fragmento de código fornecido. A variável é atribuída
nula no início do método. Além disso, em uma das primeiras condições de
troca , a variável é inicializada, após a qual o método sai. O segundo bloco de
chave contém muitas condições, em cada uma das quais a variável também recebe um novo valor. Enquanto no bloco
padrão , a variável
eventPropertyValue é usada sem nenhuma verificação, o que é um erro, pois a variável é
nula no momento.
V3066 Possível ordem incorreta de argumentos transmitida ao construtor 'EventHubConsumer': 'partitionId' e 'consumerGroup'. TrackOneEventHubClient.cs 394
public override EventHubConsumer CreateConsumer(....) { return new EventHubConsumer ( new TrackOneEventHubConsumer(....), TrackOneClient.EventHubName, partitionId,
O analisador suspeitou de ordem confusa do terceiro e quarto argumentos ao chamar o construtor da classe
EventHubConsumer . Então, vamos verificar esta declaração do construtor:
internal EventHubConsumer(TransportEventHubConsumer transportConsumer, string eventHubName, string consumerGroup, // <= 3 string partitionId, // <= 4 EventPosition eventPosition, EventHubConsumerOptions consumerOptions, EventHubRetryPolicy retryPolicy) { .... }
De fato, os argumentos são confusos. Atrevo-me a sugerir como o erro foi cometido. Talvez a formatação incorreta do código seja a responsável aqui. Basta dar uma olhada na declaração do construtor
EventHubConsumer . Devido ao fato de o primeiro parâmetro
transportConsumer estar na mesma linha com o nome da classe, pode parecer que o parâmetro
partitionId esteja em terceiro lugar, não no quarto (meus comentários com os números dos parâmetros não estão disponíveis no código original) . Isso é apenas um palpite, mas eu alteraria a formatação do código do construtor para o seguinte:
internal EventHubConsumer ( TransportEventHubConsumer transportConsumer, string eventHubName, string consumerGroup, string partitionId, EventPosition eventPosition, EventHubConsumerOptions consumerOptions, EventHubRetryPolicy retryPolicy) { .... }
Azure.Storage
V3112 Uma anormalidade em comparações semelhantes. É possível que um erro de digitação esteja presente dentro da expressão 'ContentLanguage == other.ContentEncoding'. BlobSasBuilder.cs 410
public struct BlobSasBuilder : IEquatable<BlobSasBuilder> { .... public bool Equals(BlobSasBuilder other) => BlobName == other.BlobName && CacheControl == other.CacheControl && BlobContainerName == other.BlobContainerName && ContentDisposition == other.ContentDisposition && ContentEncoding == other.ContentEncoding &&
Um erro cometido por desatenção. Encontrar esse erro com a revisão de código é bastante difícil. Aqui está a versão correta do código:
.... ContentEncoding == other.ContentEncoding && ContentLanguage == other.ContentLanguage && ....
V3112 Uma anormalidade em comparações semelhantes. É possível que um erro de digitação esteja presente dentro da expressão 'ContentLanguage == other.ContentEncoding'. FileSasBuilder.cs 265
public struct FileSasBuilder : IEquatable<FileSasBuilder> { .... public bool Equals(FileSasBuilder other) => CacheControl == other.CacheControl && ContentDisposition == other.ContentDisposition && ContentEncoding == other.ContentEncoding
Há exatamente o mesmo erro em um trecho de código muito semelhante. O código pode ter sido copiado e parcialmente alterado. Mas o erro permaneceu.
Microsoft.Azure.Batch
V3053 Uma expressão excessiva. Examine as substrings 'IList' e 'List'. PropertyData.cs 157
V3053 Uma expressão excessiva. Examine as substrings 'List' e 'IReadOnlyList'. PropertyData.cs 158
public class PropertyData { .... public bool IsTypeCollection => this.Type.Contains("IList") || this.Type.Contains("IEnumerable") || this.Type.Contains("List") ||
O analisador emitiu dois avisos sobre verificações inúteis ou erradas. No primeiro caso, procure a substring "List" depois de procurar "IList" parece redundante. É verdade, esta condição:
this.Type.Contains("IList") || this.Type.Contains("List")
pode ser bem alterado para o seguinte:
this.Type.Contains("List")
No segundo caso, a busca pela substring "IReadOnlyList" não faz sentido, pois anteriormente era procurada uma "List" de substring mais curta.
Há também uma chance de que as próprias substrings de pesquisa apresentem erros e deva haver outra coisa. De qualquer forma, apenas o autor do código sugere a versão correta, levando em consideração os dois comentários.
V3095 O objeto 'httpRequest.Content.Headers' foi usado antes de ser verificado como nulo. Verifique as linhas: 76, 79. BatchSharedKeyCredential.cs 76
public override Task ProcessHttpRequestAsync( HttpRequestMessage httpRequest, ....) { .... signature.Append(httpRequest.Content != null && httpRequest.Content.Headers.Contains("Content-Language") ? .... : ....; long? contentLength = httpRequest.Content?.Headers?.ContentLength; .... }
A variável
httpRequest.Content.Headers é usada primeiro sem nenhuma verificação, mas depois é endereçada usando o operador de acesso condicional.
V3125 O objeto 'omPropertyData' foi usado após a verificação contra nulo. Verifique as linhas: 156, 148. CodeGenerationUtilities.cs 156
private static string GetProtocolCollectionToObjectModelCollectionString( ...., PropertyData omPropertyData, ....) { if (IsMappedEnumPair(omPropertyData?.GenericTypeParameter, ....)) { .... } if (IsTypeComplex(omPropertyData.GenericTypeParameter)) .... }
E aqui está uma situação inversa. Um bloco de código contém uma variante de acesso seguro à referência potencialmente nula do
omPropertyData . Além disso, no código, essa referência é tratada sem nenhuma verificação.
V3146 Possível desreferência nula de 'valor'. O 'FirstOrDefault' pode retornar o valor nulo padrão. BatchSharedKeyCredential.cs 127
public override Task ProcessHttpRequestAsync(HttpRequestMessage httpRequest, ....) { .... foreach (string canonicalHeader in customHeaders) { string value = httpRequest.Headers. GetValues(canonicalHeader).FirstOrDefault(); value = value.Replace('\n', ' ').Replace('\r', ' ').TrimStart(); .... } .... }
Devido ao método
FirstOrDefault , se a pesquisa falhar, o valor padrão será retornado, que é
nulo para o tipo de
sequência . O valor será atribuído à variável
value , que será usada no código com o método
Replace sem nenhuma verificação. O código deve ser mais seguro. Por exemplo, da seguinte maneira:
foreach (string canonicalHeader in customHeaders) { string value = httpRequest.Headers. GetValues(canonicalHeader).FirstOrDefault(); value = value?.Replace('\n', ' ').Replace('\r', ' ').TrimStart(); .... }
Microsoft.Azure.ServiceBus
V3121 Uma enumeração 'BlocksUsing' foi declarada com o atributo 'Flags', mas não define nenhum inicializador para substituir os valores padrão. Fx.cs 69
static class Fx { .... public static class Tag { .... [Flags] public enum BlocksUsing { MonitorEnter, MonitorWait, ManualResetEvent, AutoResetEvent, AsyncResult, IAsyncResult, PInvoke, InputQueue, ThreadNeutralSemaphore, PrivatePrimitive, OtherInternalPrimitive, OtherFrameworkPrimitive, OtherInterop, Other, NonBlocking, } .... } .... }
A enumeração é declarada com o atributo
Flags . Ao mesmo tempo, os valores constantes são deixados por padrão (
MonitorEnter = 0 ,
MonitorWait = 1 ,
ManualResetEvent = 2 e assim por diante). Isso pode resultar no seguinte caso: ao tentar usar a combinação de sinalizadores, por exemplo, a segunda e a terceira constantes
MonitorWait (= 1) |
ManualResetEvent (= 2) , não será recebido um valor exclusivo, mas a constante com o valor 3 por padrão
(AutoResetEvent ). Isso pode ser uma surpresa para o código do chamador. Se a enumeração
BlocksUsing for realmente usada para definir combinações de sinalizadores (campo de bit), as constantes devem receber valores iguais ao número com potências de dois.
[Flags] public enum BlocksUsing { MonitorEnter = 1, MonitorWait = 2, ManualResetEvent = 4, AutoResetEvent = 8, AsyncResult = 16, IAsyncResult = 32, PInvoke = 64, InputQueue = 128, ThreadNeutralSemaphore = 256, PrivatePrimitive = 512, OtherInternalPrimitive = 1024, OtherFrameworkPrimitive = 2048, OtherInterop = 4096, Other = 8192, NonBlocking = 16384, }
V3125 O objeto 'session' foi usado depois que foi verificado com relação a nulo. Verifique as linhas: 69, 68. AmqpLinkCreator.cs 69
public async Task<Tuple<AmqpObject, DateTime>> CreateAndOpenAmqpLinkAsync() { .... AmqpSession session = null; try {
Preste atenção à manipulação de variáveis da
sessão no bloco de
captura . O método
Abort é chamado com segurança pelo operador de acesso condicional. Mas depois que o método
GetInnerException é chamado sem segurança. Ao fazer isso,
NullReferenceException pode ser lançado em vez de uma exceção do tipo esperado. Este código deve ser corrigido. O método
AmqpExceptionHelper.GetClientException suporta a passagem do valor
nulo para o parâmetro
innerException :
public static Exception GetClientException( Exception exception, string referenceId = null, Exception innerException = null, bool connectionError = false) { .... }
Portanto, pode-se usar apenas o operador de acesso condicional ao chamar
session.GetInnerException () :
public async Task<Tuple<AmqpObject, DateTime>> CreateAndOpenAmqpLinkAsync() { .... AmqpSession session = null; try {
Conclusão
Como você pode ver, um tamanho grande de projeto nem sempre garante muitos erros. No entanto, permanecemos alertas, pois sempre podemos encontrar algo. Mesmo em um projeto tão complexo como o SDK do Azure para .NET. Encontrar alguns defeitos cruciais requer esforços adicionais. Mas quanto mais dificuldades, mais agradável é o resultado. Por outro lado, para evitar esforços indevidos, recomendamos o uso de análise estática nos computadores dos desenvolvedores ao escrever um novo código. Essa é a abordagem mais eficaz.
Faça o download e experimente o PVS-Studio em ação. Boa sorte na luta contra insetos!