
Periodicamente, voltamos aos projetos que testamos anteriormente com o PVS-Studio e escrevemos artigos sobre o assunto. Há duas razões para fazer isso. Primeiro, para entender o quão melhor nosso analisador se tornou. Em segundo lugar, para rastrear se os autores do projeto prestaram atenção ao nosso artigo, bem como ao relatório de erros que geralmente fornecemos a eles. Obviamente, os erros podem ser corrigidos sem a nossa participação. Mas é sempre bom quando exatamente nossos esforços ajudam a melhorar um projeto. Roslyn não foi exceção. Um artigo de revisão anterior deste projeto data de 23 de dezembro de 2015. Isso é bastante tempo, considerando o caminho que nosso analisador percorreu em seu desenvolvimento durante esse período. Para nós, pessoalmente, Roslyn também é de interesse adicional pelo fato de o núcleo do analisador C # PVS-Studio se basear nele. Portanto, estamos muito interessados na qualidade do código para este projeto. Organizaremos uma segunda verificação e descobriremos o que há de novo e interessante (mas esperemos que nada significativo) o PVS-Studio possa encontrar lá.
Roslyn (ou a plataforma do compilador .NET) provavelmente é familiar para muitos de nossos leitores. Em resumo, é um conjunto de APIs e compiladores de código aberto para análise de código para as linguagens C # e Visual Basic .NET da Microsoft. O código fonte do projeto está disponível no
GitHub .
Não vou dar uma descrição detalhada dessa plataforma, mas eu recomendaria a todos os interessados um artigo do meu colega Sergey Vasiliev, "
Introdução ao Roslyn. Usando ferramentas de análise estática para desenvolver ". Neste artigo, você pode aprender não apenas sobre os recursos da arquitetura Roslyn, mas também como exatamente usamos essa plataforma.
Como mencionei anteriormente, mais de três anos se passaram desde a redação do último artigo de meu colega Andrei Karpov sobre a verificação de Roslyn "
Lançamento de ano novo do PVS-Studio 6.00: verificação de Roslyn ". Durante esse período, o analisador C # PVS-Studio adquiriu muitos recursos novos. Em geral, o artigo de Andrey era uma espécie de "bola de teste", porque o analisador C # foi adicionado apenas ao PVS-Studio. Apesar disso, mesmo assim, em um projeto incondicionalmente de alta qualidade, Roslyn conseguiu encontrar erros interessantes. O que mudou no analisador para o código C # até agora, o que potencialmente permitirá uma análise mais profunda?
Nos últimos tempos, o núcleo do analisador e a infraestrutura foram desenvolvidos. Foi adicionado suporte para o Visual Studio 2017 e Roslyn 2.0, além de profunda integração com o MSBuild. Você pode ler mais sobre nossa abordagem de integração com o MSBuild e sobre os motivos que nos levaram a aceitá-la no artigo de meu colega Pavel Yeremeyev, "
Suporte para Visual Studio 2017 e Roslyn 2.0 no PVS-Studio: às vezes, usar soluções prontas não é tão fácil quanto parece. de relance . "
Agora, estamos trabalhando ativamente na transição para o Roslyn 3.0, de acordo com o mesmo esquema em que inicialmente apoiamos o Visual Studio 2017, ou seja, por meio de nosso próprio conjunto de ferramentas, que vem no kit de distribuição PVS-Studio com um "esboço" na forma de um arquivo MSBuild.exe vazio. Apesar de parecer uma "muleta" (a API do MSBuild não é muito amigável para reutilização em projetos de terceiros patentes devido à baixa portabilidade das bibliotecas), essa abordagem já nos ajudou a reviver várias atualizações indolores de Roslyn durante a vida do Visual Studio 2017, de maneira relativamente indolor. e agora, embora com muitas sobreposições, sobreviva à atualização para o Visual Studio 2019, além de manter a compatibilidade e o desempenho com versões anteriores em sistemas com versões mais antigas do MSBuild.
O núcleo do analisador também passou por várias melhorias. Uma das principais inovações é uma análise interprocedual completa, levando em consideração os valores de entrada e saída dos métodos, levando em consideração, dependendo desses parâmetros, a acessibilidade dos ramos de execução e pontos de retorno.
A tarefa de rastrear parâmetros dentro dos métodos já está quase concluída, preservando as anotações automáticas para o que acontece com esses parâmetros (por exemplo, uma desreferência potencialmente perigosa). Isso permitirá que qualquer diagnóstico que use o mecanismo de fluxo de dados leve em consideração situações perigosas que ocorrem ao passar um parâmetro para um método. Anteriormente, ao analisar locais tão perigosos, um aviso não era gerado, pois não podíamos conhecer todos os possíveis valores de entrada para esse método. Agora podemos detectar o perigo, pois em todos os locais em que esse método é chamado, esses parâmetros de entrada serão levados em consideração.
Nota: você pode se familiarizar com os principais mecanismos do analisador, como fluxo de dados e outros, no artigo "
Tecnologias usadas no analisador de código PVS-Studio para encontrar erros e vulnerabilidades em potencial ".
A análise interprocedural no PVS-Studio C # não é limitada pelos parâmetros de entrada ou pela profundidade. A única limitação são os métodos virtuais em classes que não foram fechadas por herança e caíram em recursão (pararemos quando vimos na pilha uma chamada repetida para um método já calculado). Além disso, o próprio método recursivo será finalmente calculado com a suposição de que o valor de retorno de sua auto-recursão é desconhecido.
Outra grande inovação no analisador C # é a possibilidade de desreferenciar um ponteiro potencialmente nulo. Anteriormente, o analisador jurava uma possível exceção de referência nula se tivesse certeza de que em todas as ramificações de execução o valor da variável seria nulo. Obviamente, ele às vezes estava enganado; portanto, o diagnóstico do
V3080 era anteriormente chamado de referência nula potencial.
Agora, o analisador lembra que a variável pode ser nula em uma das ramificações de execução (por exemplo, sob uma determinada condição em if). Se ele vir acesso a essa variável sem verificar, receberá uma mensagem V3080, mas com um nível de importância menor do que se vir nulo em todas as ramificações. Em combinação com a análise interprocedural aprimorada, esse mecanismo permite encontrar muito difícil detectar erros. Um exemplo é uma longa cadeia de chamadas de método, a última com a qual você não está familiarizado e que, por exemplo, retorna nulo em determinadas circunstâncias, mas você não se protegeu disso porque simplesmente não a conhecia. Nesse caso, o analisador jura apenas quando vê precisamente a atribuição de nulo. Em nossa opinião, isso distingue qualitativamente nossa abordagem de uma inovação do C # 8.0 como o tipo de referência anulável, que, de fato, se resume a definir verificações nulas em cada método. Oferecemos uma alternativa - fazer verificações apenas onde o nulo pode realmente chegar, e nosso analisador agora pode procurar essas situações.
Portanto, sem demora, passemos ao "interrogatório" - analisando os resultados da verificação de Roslyn. Primeiro, vamos olhar para os erros encontrados graças às inovações descritas acima. Em geral, alguns avisos foram emitidos para o código Roslyn dessa vez. Acho que isso se deve ao fato de a plataforma estar se desenvolvendo muito ativamente (atualmente, a base de código é de cerca de 2.770.000 linhas de código, excluindo as vazias) e não analisamos esse projeto há muito tempo. No entanto, não existem muitos erros críticos, ou seja, eles são de interesse do artigo. E sim, existem alguns testes em Roslyn que eu, como sempre, excluí dos testes.
Iniciarei com erros V3080, com nível de criticidade Médio, no qual o analisador detectou um possível acesso por um link zero, mas não em todos os casos possíveis (ramificações de código).
Possível desreferência nula - MédiaV3080 Possível desreferência nula. Considere inspecionar 'atual'. CSharpSyntaxTreeFactoryService.PositionalSyntaxReference.cs 70
private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....))
Considere o método
GetNode . O analisador considera que o acesso por referência nula é possível na condição do bloco
while . No corpo do bloco
while , a variável
atual receberá um valor - o resultado da execução do método
AsNode . E esse valor em alguns casos será
nulo . Um bom exemplo de análise interprocedural em ação.
Agora considere um caso semelhante no qual a análise interprocedural foi realizada por meio de duas chamadas de método.
V3080 Possível desreferência nula. Considere inspecionar 'diretório'. CommonCommandLineParser.cs 911
private IEnumerable<CommandLineSourceFile> ExpandFileNamePattern(string path, string baseDirectory, ....) { string directory = PathUtilities.GetDirectoryName(path); .... var resolvedDirectoryPath = (directory.Length == 0) ?
A variável de
diretório no corpo do método
ExpandFileNamePattern obtém o valor do método
GetDirectoryName (string) . Isso, por sua vez, retornará o resultado do método
GetDirectoryName (string, bool) sobrecarregado, cujo valor pode ser
nulo . Como mais adiante no corpo do método
ExpandFileNamePattern a variável de
diretório é usada sem primeiro verificar a igualdade
nula , podemos falar sobre a legitimidade do aviso pelo analisador. Este é um design potencialmente inseguro.
Outro trecho de código com o erro V3080, mais precisamente, imediatamente com dois erros emitidos para uma linha de código. Aqui, a análise interprocedural não era necessária.
V3080 Possível desreferência nula. Considere inspecionar 'spanStartLocation'. TestWorkspace.cs 574
V3080 Possível desreferência nula. Considere inspecionar 'spanEndLocationExclusive'. TestWorkspace.cs 574
private void MapMarkupSpans(....) { .... foreach (....) { .... foreach (....) { .... int? spanStartLocation = null; int? spanEndLocationExclusive = null; foreach (....) { if (....) { if (spanStartLocation == null && positionInMarkup <= markupSpanStart && ....) { .... spanStartLocation = ....; } if (spanEndLocationExclusive == null && positionInMarkup <= markupSpanEndExclusive && ....) { .... spanEndLocationExclusive = ....; break; } .... } .... } tempMappedMarkupSpans[key]. Add(new TextSpan( spanStartLocation.Value,
As
variáveis spanStartLocation e
spanEndLocationExclusive são do tipo
nulo int e são inicializadas para
nulo . Além disso, no código, eles podem receber valores, mas somente se determinadas condições forem atendidas. Em alguns casos, seu valor permanecerá igual a
nulo . Além disso, no código, essas variáveis são acessadas por referência sem primeiro verificar a igualdade
nula , como indica o analisador.
O código de Roslyn contém alguns desses erros, mais de 100. Geralmente, o padrão desses erros é o mesmo. Existe algum método geral que potencialmente retorna
nulo . O resultado desse método é usado em um grande número de locais, às vezes através de dezenas de chamadas de método intermediárias ou verificações adicionais. É importante entender que esses erros não são fatais, mas podem levar ao acesso via link nulo. E detectar esses erros é muito difícil. Portanto, em alguns casos, considere refatorar o código, no qual uma exceção seria lançada se o método retornasse
nulo . Caso contrário, você pode proteger seu código apenas com verificações totais, o que é bastante tedioso e não confiável. Obviamente, em cada caso, a decisão deve ser tomada com base nas características do projeto.
Nota Ocorre que, no momento, não há situações (dados de entrada) em que o método retorne
nulo e não haja erro real. No entanto, esse código ainda não é confiável, pois tudo pode mudar quando são feitas alterações no código.
Para fechar o tópico com a
V3080 , vamos dar uma olhada nos erros óbvios com o alto nível de confiança, quando o acesso por um link nulo é mais provável ou até inevitável.
Possível desreferência nula - AltaV3080 Possível desreferência nula. Considere inspecionar 'collectionType.Type'. AbstractConvertForToForEachCodeRefactoringProvider.cs 137
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { .... var collectionType = semanticModel.GetTypeInfo(....); if (collectionType.Type == null && collectionType.Type.TypeKind == TypeKind.Error) { return; } .... }
Devido a um erro de digitação na condição (em vez do operador
|| que usamos
&& ), o código não funciona conforme o esperado e a variável
collectionType.Type será acessada se for
nula . A condição deve ser corrigida da seguinte maneira:
if (collectionType.Type == null || collectionType.Type.TypeKind == TypeKind.Error) ....
A propósito, a segunda variante do desenvolvimento de eventos também é possível: na primeira parte, as condições são misturadas pelos operadores
== e
! = . Em seguida, o código corrigido seria:
if (collectionType.Type != null && collectionType.Type.TypeKind == TypeKind.Error) ....
Esta versão do código é menos lógica, mas também corrige o erro. A decisão final é com os autores do projeto.
Outro erro semelhante.
V3080 Possível desreferência nula. Considere inspecionar 'ação'. TextViewWindow_InProc.cs 372
private Func<IWpfTextView, Task> GetLightBulbApplicationAction(....) { .... if (action == null) { throw new InvalidOperationException( $"Unable to find FixAll in {fixAllScope.ToString()} code fix for suggested action '{action.DisplayText}'."); } .... }
Ocorreu um erro ao compor uma mensagem para uma exceção. Ao mesmo tempo, é feita uma tentativa de acessar a propriedade
action.DisplayText através da variável
action , que obviamente é
nula .
E o último erro é o nível alto do V3080.
V3080 Possível desreferência nula. Considere inspecionar 'tipo'. ObjectFormatterHelpers.cs 91
private static bool IsApplicableAttribute( TypeInfo type, TypeInfo targetType, string targetTypeName) { return type != null && AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName; }
O método é pequeno, então eu dou todo o código. A condição no bloco de
devolução está incorreta. Em alguns casos, é possível lançar uma
NullReferenceException ao acessar
type.FullName . Eu uso colchetes (eles não mudarão de comportamento aqui) para esclarecer a situação:
return (type != null && AreEquivalent(targetType, type)) || (targetTypeName != null && type.FullName == targetTypeName);
É assim que, de acordo com a prioridade das operações, esse código funcionará. Se a variável
type for
nula , entramos em uma verificação else, onde, certificando-se de que a variável
targetTypeName seja
nula , usamos a referência de
tipo nula. Você pode corrigir o código, por exemplo, assim:
return type != null && (AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName);
Acho que é aí que você pode concluir o estudo dos erros do V3080 e ver o que mais o analisador interessante do PVS-Studio conseguiu encontrar no código de Roslyn.
TypoV3005 A variável 'SourceCodeKind' é atribuída a si mesma. DynamicFileInfo.cs 17
internal sealed class DynamicFileInfo { .... public DynamicFileInfo( string filePath, SourceCodeKind sourceCodeKind, TextLoader textLoader, IDocumentServiceProvider documentServiceProvider) { FilePath = filePath; SourceCodeKind = SourceCodeKind;
Devido a um nome de variável malsucedido, um erro de digitação foi feito no construtor da classe
DynamicFileInfo . O
campo SourceCodeKind recebe seu próprio valor, em vez de usar o parâmetro
sourceCodeKind . Para minimizar a probabilidade de tais erros, é recomendável usar o prefixo de sublinhado para nomes de parâmetros nesses casos. Vou dar um exemplo de uma versão corrigida do código:
public DynamicFileInfo( string _filePath, SourceCodeKind _sourceCodeKind, TextLoader _textLoader, IDocumentServiceProvider _documentServiceProvider) { FilePath = _filePath; SourceCodeKind = _sourceCodeKind; TextLoader = _textLoader; DocumentServiceProvider = _documentServiceProvider; }
DescuidoV3006 O objeto foi criado, mas não está sendo usado. A palavra-chave 'throw' pode estar ausente: throw new InvalidOperationException (FOO). ProjectBuildManager.cs 61
~ProjectBuildManager() { if (_batchBuildStarted) { new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } }
Sob uma determinada condição, o destruidor deve lançar uma exceção, mas isso não acontece, e o objeto de exceção é simplesmente criado. A palavra-chave
throw foi omitida. Versão corrigida do código:
~ProjectBuildManager() { if (_batchBuildStarted) { throw new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } }
A questão de trabalhar com destruidores em C # e lançar exceções a eles é um tópico para uma discussão separada, que está além do escopo deste artigo.
Quando o resultado não é importanteVários avisos do
V3009 foram recebidos para métodos que retornam o mesmo valor em todos os casos. Às vezes, isso não é crítico ou o código de retorno simplesmente não é verificado no código de chamada. Eu perdi esses avisos. Mas alguns pedaços de código pareciam suspeitos para mim. Vou citar um deles:
V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. GoToDefinitionCommandHandler.cs 62
internal bool TryExecuteCommand(....) { .... using (context.OperationContext.AddScope(....)) { if (....) { return true; } } .... return true; }
O método
TryExecuteCommand retorna apenas
true e nada além de
true . Ao mesmo tempo, o valor de retorno está envolvido em algumas verificações no código de chamada:
public bool ExecuteCommand(....) { .... if (caretPos.HasValue && TryExecuteCommand(....)) { .... } .... }
É difícil dizer o quão perigoso é esse comportamento. Mas se o resultado não for necessário, pode valer a pena substituir o tipo de retorno por void e fazer alterações mínimas no método de chamada. Isso tornará o código mais compreensível e seguro.
Outros avisos semelhantes:
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. CommentUncommentSelectionCommandHandler.cs 86
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. RenomearTrackingTaggerProvider.RenameTrackingCommitter.cs 99
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. JsonRpcClient.cs 138
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. AbstractFormatEngine.OperationApplier.cs 164
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'false'. TriviaDataFactory.CodeShapeAnalyzer.cs 254
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. ObjectList.cs 173
- V3009 É estranho que esse método sempre retorne um e o mesmo valor de 'true'. ObjectList.cs 249
Não verificadoV3019 Possivelmente, uma variável incorreta é comparada com nulo após a conversão do tipo usando a palavra-chave 'as'. Verifique as variáveis 'valor', 'valueToSerialize'. RoamingVisualStudioProfileOptionPersister.cs 277
public bool TryPersist(OptionKey optionKey, object value) { .... var valueToSerialize = value as NamingStylePreferences; if (value != null) { value = valueToSerialize.CreateXElement().ToString(); } .... }
O
valor da variável é do tipo
NamingStylePreferences . O problema está seguindo essa verificação. Mesmo que a variável
value não seja nula, isso não garante que a conversão do tipo tenha sido bem-sucedida e que
valueToSerialize não contenha
nulo . Uma
NullReferenceException pode ser lançada . O código precisa ser corrigido da seguinte maneira:
var valueToSerialize = value as NamingStylePreferences; if (valueToSerialize != null) { value = valueToSerialize.CreateXElement().ToString(); }
E mais um erro semelhante.
V3019 Possivelmente, uma variável incorreta é comparada com nulo após a conversão do tipo usando a palavra-chave 'as'. Verifique as variáveis 'columnState', 'columnState2'. StreamingFindUsagesPresenter.cs 181
private void SetDefinitionGroupingPriority(....) { .... foreach (var columnState in ....) { var columnState2 = columnState as ColumnState2; if (columnState?.Name ==
A variável
columnState é do tipo
ColumnState2 . No entanto, o resultado da operação, a variável
columnState2 , não é mais verificado quanto a
nulo . Em vez disso, a variável
columnState é verificada usando a instrução
nula condicional. Qual é o perigo desse código? Como no exemplo anterior, a conversão de tipo usando o operador as pode falhar e a variável
columnState2 será
nula , o que lançará uma exceção. A propósito, um erro de digitação pode ser o culpado. Observe a condição no bloco
if . Talvez em vez de
columnState? .Name eles quisessem escrever
columnState2? .Name . Isso é muito provável, devido aos nomes de variáveis bastante infelizes
columnState e columnState2.Verificações redundantesUm número bastante grande de avisos (acima de 100) foi emitido para construções não críticas, mas potencialmente inseguras associadas a verificações redundantes. Por exemplo, darei um deles.
A expressão
V3022 'navInfo == null' sempre é falsa. AbstractSyncClassViewCommandHandler.cs 101
public bool ExecuteCommand(....) { .... IVsNavInfo navInfo = null; if (symbol != null) { navInfo = libraryService.NavInfoFactory.CreateForSymbol(....); } if (navInfo == null) { navInfo = libraryService.NavInfoFactory.CreateForProject(....); } if (navInfo == null)
Talvez não haja nenhum erro real aqui. Apenas uma boa razão para demonstrar a combinação de tecnologias "análise interprocedural + análise de fluxo de dados" em ação. O analisador considera que a segunda verificação
navInfo == null é redundante. De fato, antes disso, o valor para atribuir
navInfo será obtido a partir do método
libraryService.NavInfoFactory.CreateForProject , que construirá e retornará um novo objeto da classe
NavInfo . Mas não
nulo de forma alguma. A questão é: por que o analisador não
gerou um aviso para a primeira verificação
navInfo == null ? Há uma explicação para isso. Primeiro, se a variável do
símbolo for
nula , o valor de
navInfo permanecerá uma referência nula. Em segundo lugar, mesmo se
navInfo obtiver o valor do método
libraryService.NavInfoFactory.CreateForSymbol , esse valor poderá ser
nulo . Portanto, a primeira verificação de
navInfo == null é realmente necessária.
Cheques insuficientesE agora a situação é o inverso do acima. Vários avisos da
V3042 foram recebidos para código que pode ser acessado por uma referência nula. Além disso, apenas uma ou duas pequenas verificações poderiam consertar tudo.
Considere um pedaço interessante de código que contém dois desses erros.
V3042 Possível NullReferenceException. O '?' e '.' operadores são usados para acessar membros do objeto 'receptor' Binder_Expressions.cs 7770
V3042 Possível NullReferenceException. O '?' e '.' operadores são usados para acessar membros do objeto 'receptor' Binder_Expressions.cs 7776
private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax !=
A variável do
receptor pode ser
nula . O autor do código sabe disso porque ele usa o operador
nulo condicional na condição do bloco
if para acessar o
receptor? .Sintaxe . Além disso, no código, o
receptor variável
é usado sem nenhuma verificação para acessar o
receiver.Type ,
receiver.Syntax e
receiver.HasErrors . Esses erros precisam ser corrigidos:
private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax != GetConditionalReceiverSyntax(conditionalAccessNode)) { receiver = BindConditionalAccessReceiver(conditionalAccessNode, diagnostics); } var receiverType = receiver?.Type; if (receiverType?.IsNullableType() == true) { .... } receiver = new BoundConditionalReceiver(receiver?.Syntax, 0, receiverType ?? CreateErrorType(), hasErrors: receiver?.HasErrors) { WasCompilerGenerated = true }; return receiver; }
Você também precisa garantir que o construtor
BoundConditionalReceiver suporte a obtenção de valores
nulos para seus parâmetros ou execute refatoração adicional.
Outros erros semelhantes:
- V3042 Possível NullReferenceException. O '?' e '.' operadores são usados para acessar membros do objeto 'containsType' SyntaxGeneratorExtensions_Negate.cs 240
- V3042 Possível NullReferenceException. O '?' e '.' operadores são usados para acessar membros do objeto 'expressão' ExpressionSyntaxExtensions.cs 349
- V3042 Possível NullReferenceException. O '?' e '.' operadores são usados para acessar membros do objeto 'expressão' ExpressionSyntaxExtensions.cs 349
Erro na condiçãoV3057 A função '
Substring ' pode receber o valor '-1' enquanto se espera um valor não negativo. Inspecione o segundo argumento. CommonCommandLineParser.cs 109
internal static bool TryParseOption(....) { .... if (colon >= 0) { name = arg.Substring(1, colon - 1); value = arg.Substring(colon + 1); } .... }
Se a variável de
dois pontos for 0, o que permite uma condição no código, o método
Substring lançará uma
ArgumentOutOfRangeException . Correção necessária:
if (colon > 0)
Erro de digitação é possívelV3065 O parâmetro 't2' não é utilizado dentro do corpo do método. CSharpCodeGenerationHelpers.cs 84
private static TypeDeclarationSyntax ReplaceUnterminatedConstructs(....) { .... var updatedToken = lastToken.ReplaceTrivia(lastToken.TrailingTrivia, (t1, t2) => { if (t1.Kind() == SyntaxKind.MultiLineCommentTrivia) { var text = t1.ToString(); .... } else if (t1.Kind() == SyntaxKind.SkippedTokensTrivia) { return ReplaceUnterminatedConstructs(t1); } return t1; }); .... }
Dois parâmetros são passados para a expressão lambda: t1 e t2. No entanto, apenas t1 é usado. Parece suspeito, considerando como é fácil cometer um erro ao usar variáveis com esses nomes.
DescuidoV3083 Chamada não segura do evento 'TagsChanged', NullReferenceException é possível. Considere atribuir um evento a uma variável local antes de invocá-lo. PreviewUpdater.Tagger.cs 37
public void OnTextBufferChanged() { if (PreviewUpdater.SpanToShow != default) { if (TagsChanged != null) { var span = _textBuffer.CurrentSnapshot.GetFullSpan(); TagsChanged(this, new SnapshotSpanEventArgs(span));
O evento
TagsChanged é acionado de maneira insegura. Entre a verificação da igualdade
nula e a chamada de um evento, eles podem ter tempo para cancelar a inscrição, e uma exceção será lançada. Além disso, no corpo do bloco
if , imediatamente antes da chamada do evento, algumas outras operações são feitas. Chamei esse erro de "Desatenção", porque em outros lugares do código eles trabalham com esse evento com mais precisão, por exemplo, assim:
private void OnTrackingSpansChanged(bool leafChanged) { var handler = TagsChanged; if (handler != null) { var snapshot = _buffer.CurrentSnapshot; handler(this, new SnapshotSpanEventArgs(snapshot.GetFullSpan())); } }
O uso da variável
manipuladora opcional elimina o problema. No método
OnTextBufferChanged ,
você precisa fazer edições para a mesma operação segura com o evento.
Interseção de intervalosV3092 As interseções de faixa são possíveis dentro de expressões condicionais. Exemplo: if (A> 0 && A <5) {...} else if (A> 3 && A <9) {...}. ILBuilderEmit.cs 677
internal void EmitLongConstant(long value) { if (value >= int.MinValue && value <= int.MaxValue) { .... } else if (value >= uint.MinValue && value <= uint.MaxValue) { .... } else { .... } }
Para uma melhor compreensão, reescreverei esse fragmento de código, substituindo os nomes das constantes pelos valores reais:
internal void EmitLongConstant(long value) { if (value >= -2147483648 && value <= 2147483648) { .... } else if (value >= 0 && value <= 4294967295) { .... } else { .... } }
Provavelmente, não há nenhum erro real, mas a condição parece estranha. A segunda parte (
senão se ) será executada apenas para o intervalo de valores de 2147483648 + 1 a 4294967295.
Mais alguns destes avisos:
- V3092 As interseções de faixa são possíveis dentro de expressões condicionais. Exemplo: if (A> 0 && A <5) {...} else if (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 109
- V3092 As interseções de faixa são possíveis dentro de expressões condicionais. Exemplo: if (A> 0 && A <5) {...} else if (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 66
Mais sobre verificações de igualdade nulas (ou a falta delas)Alguns erros do
V3095 sobre a verificação de uma variável como
nula após o uso. O primeiro é ambíguo, considere o código.
V3095 O objeto 'displayName' foi usado antes de ser verificado contra nulo. Verifique as linhas: 498, 503. FusionAssemblyIdentity.cs 498
internal static IAssemblyName ToAssemblyNameObject(string displayName) { if (displayName.IndexOf('\0') >= 0) { return null; } Debug.Assert(displayName != null); .... }
Supõe-se que a referência
displayName possa ser nula. Para fazer isso, verifique
Debug.Assert . Não está claro por que isso ocorre depois de usar a string. Também deve ser
observado que, para outras configurações que não sejam Debug, o compilador
removerá o Debug.Assert do código. Isso significa que apenas para Debug é possível obter uma referência nula? E se não for assim, por que não verificou
string.IsNullOrEmpty (string) , por exemplo. Essas são perguntas para os autores do código.
O seguinte erro é mais óbvio.
V3095 O objeto 'scriptArgsOpt' foi usado antes de ser verificado em relação a nulo. Verifique as linhas: 321, 325. CommonCommandLineParser.cs 321
internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt.Add(arg);
Eu acho que esse pedaço de código não requer explicações. Vou dar a versão corrigida:
internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt?.Add(arg); continue; } if (scriptArgsOpt != null) { .... } .... } }
O código Roslyn encontrou outros 15 desses erros:
- V3095 O objeto 'LocalFunctions' foi usado antes de ser verificado contra nulo. Verifique as linhas: 289, 317. ControlFlowGraphBuilder.RegionBuilder.cs 289
- V3095 O objeto 'resolution.OverloadResolutionResult' foi usado antes de ser verificado como nulo. Verifique as linhas: 579, 588. Binder_Invocation.cs 579
- V3095 O objeto 'resolution.MethodGroup' foi usado antes de ser verificado contra nulo. Verifique as linhas: 592, 621. Binder_Invocation.cs 592
- V3095 O objeto 'touchedFilesLogger' foi usado antes de ser verificado como nulo. Verifique as linhas: 111, 126. CSharpCompiler.cs 111
- V3095 O objeto 'newExceptionRegionsOpt' foi usado antes de ser verificado como nulo. Verifique as linhas: 736, 743. AbstractEditAndContinueAnalyzer.cs 736
- V3095 O objeto 'symbol' foi usado antes de ser verificado com relação a nulo. Verifique as linhas: 422, 427. AbstractGenerateConstructorService.Editor.cs 422
- V3095 O objeto '_state.BaseTypeOrInterfaceOpt' foi usado antes de ser verificado como nulo. Verifique as linhas: 132, 140. AbstractGenerateTypeService.GenerateNamedType.cs 132
- V3095 O objeto 'elemento' foi usado antes de ser verificado com relação a nulo. Verifique as linhas: 232, 233. ProjectUtil.cs 232
- V3095 O objeto 'languages' foi usado antes de ser verificado com valor nulo. Verifique as linhas: 22, 28. ExportCodeCleanupProvider.cs 22
- V3095 O objeto 'memberType' foi usado antes de ser verificado com relação a nulo. Verifique as linhas: 183, 184. SyntaxGeneratorExtensions_CreateGetHashCodeMethod.cs 183
- V3095 O objeto 'validTypeDeclarations' foi usado antes de ser verificado em relação a nulo. Check lines: 223, 228. SyntaxTreeExtensions.cs 223
- V3095 The 'text' object was used before it was verified against null. Check lines: 376, 385. MSBuildWorkspace.cs 376
- V3095 The 'nameOrMemberAccessExpression' object was used before it was verified against null. Check lines: 206, 223. CSharpGenerateTypeService.cs 206
- V3095 The 'simpleName' object was used before it was verified against null. Check lines: 83, 85. CSharpGenerateMethodService.cs 83
- V3095 The 'option' object was used before it was verified against null. Check lines: 23, 28. OptionKey.cs 23
Considere os erros do V3105 . Aqui, usamos o operador nulo condicional ao inicializar a variável e, a seguir, no código, a variável é usada sem verificar a igualdade nula .O próximo erro é sinalizado imediatamente por dois avisos.V3105 A variável 'documentId' foi usada após ser atribuída por meio do operador condicional nulo. NullReferenceException é possível. CodeLensReferencesService.cs 138V3105 A variável 'documentId' foi usada após ser atribuída por meio do operador condicional nulo. NullReferenceException é possível. CodeLensReferencesService.cs 139 private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingSymbolAsync(....) { .... var documentId = solution.GetDocument(location.SourceTree)?.Id; return new ReferenceLocationDescriptor( .... documentId.ProjectId.Id, documentId.Id, ....); }
A variável documentId pode ser inicializada como nula . Como resultado, a criação do ReferenceLocationDescriptor acabará lançando uma exceção. O código precisa ser corrigido: return new ReferenceLocationDescriptor( .... documentId?.ProjectId.Id, documentId?.Id, ....);
Além disso, além do código, é necessário prever a possibilidade de igualdade de variáveis nulas passadas para o construtor.Outros erros semelhantes no código:- V3105 A variável 'symbol' foi usada após ser atribuída através do operador condicional nulo. NullReferenceException é possível. SymbolFinder_Hierarchy.cs 44
- V3105 A variável 'symbol' foi usada após ser atribuída através do operador condicional nulo. NullReferenceException é possível. SymbolFinder_Hierarchy.cs 51
Prioridades e colchetesV3123 Talvez o operador '?:' Funcione de maneira diferente do esperado. Sua prioridade é menor que a prioridade de outros operadores em sua condição. Edit.cs 70 public bool Equals(Edit<TNode> other) { return _kind == other._kind && (_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode) && (_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode); }
A condição no bloco de retorno não é calculada como pensava o desenvolvedor. Supôs- se que a primeira condição seria _tipo == outro._kin d (portanto, o agrupamento de linhas foi feito após essa condição) e, em seguida, os blocos de condições com o operador " ? " Seriam calculados sequencialmente . De fato, a primeira condição será _kind == other._kind && (_oldNode == null) . Isso ocorre porque o operador && tem uma prioridade mais alta que o operador " ? ". Para corrigir o erro, é necessário agrupar todas as expressões do operador " ? ": return _kind == other._kind && ((_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode)) && ((_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode));
Isso conclui a descrição dos erros encontrados.ConclusõesApesar do número significativo de erros que pude detectar, em termos do tamanho do código do projeto Roslyn (2.770.000 linhas), isso será uma quantia bastante pequena. Como Andrei no artigo anterior, também estou pronto para reconhecer a alta qualidade deste projeto.Quero observar que essas verificações ocasionais de código não têm nada a ver com a metodologia da análise estática e praticamente não trazem nenhum benefício. A análise estática deve ser aplicada regularmente, e não de caso para caso. Muitos erros serão corrigidos nos estágios iniciais e, portanto, o custo para corrigi-los será dez vezes menor. Essa idéia é descrita em mais detalhes neste pequeno artigo , com o qual peço que você se familiarize.Você pode procurar por mais erros de forma independente no projeto considerado e em qualquer outro. Para fazer isso, basta fazer o download e experimentar o nosso analisador.
Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Sergey Khrenov. Verificando o código-fonte da Roslyn .