Quão estranho código oculta erros? Análise do TensorFlow.NET

TensorFlow.NET e PVS-Studio

A análise estática é uma ferramenta extremamente útil para qualquer desenvolvedor, pois ajuda a encontrar a tempo não apenas erros, mas também apenas pedaços de código suspeitos e estranhos que podem causar confusão para os programadores que precisariam trabalhar com ele no futuro. Essa idéia será demonstrada por uma análise do projeto aberto C # do TensorFlow.NET, que está sendo desenvolvido para trabalhar com a popular biblioteca de aprendizado de máquina do TensorFlow.

Meu nome é Nikita Lipilin. Há algum tempo, entrei para o departamento de programadores em C # do PVS-Studio. Tradicionalmente, todos os recém-chegados à equipe escrevem artigos que discutem os resultados da verificação de vários projetos abertos usando o analisador estático PVS-Studio. Esses artigos ajudam os novos funcionários a conhecer melhor o produto e, ao mesmo tempo, fornecem benefícios adicionais em termos de popularização da metodologia de análise estática. Sugiro que você se familiarize com meu primeiro trabalho sobre o tópico de análise de projetos abertos.

Introdução


A variedade de possíveis erros no código do programa é incrível. Alguns deles se revelam imediatamente, após um exame superficial do aplicativo criado, outros podem ser difíceis de perceber, mesmo ao realizar uma revisão de código por uma equipe de desenvolvedores experientes. No entanto, também acontece que, devido ao descuido ou por algum outro motivo, o programador às vezes escreve simplesmente código estranho e ilógico, que, no entanto, (parece) cumpre com êxito sua função. Porém, somente depois, ao retornar ao que foi escrito ou quando outros estudam esse código, começam a surgir perguntas que não podem ser respondidas.

A refatoração de código antigo pode se transformar em problemas, especialmente se outras partes do programa dependem dele; portanto, quando você encontra até mesmo algumas construções francamente curvadas, o ″ Funciona? Não toque! ″. Posteriormente, isso dificulta o estudo da fonte e, portanto, torna-se difícil expandir os recursos disponíveis. A base de código está entupida, há uma probabilidade de que algum problema interno pequeno e invisível, mas potencialmente muito desagradável, não seja corrigido a tempo.

Em algum momento, as consequências desse erro serão sentidas, mas sua pesquisa levará muito tempo, porque as suspeitas do desenvolvedor caem em um grande número de fragmentos de código estranhos que não foram refatorados ao mesmo tempo. Daqui resulta que vários problemas e esquisitices em um fragmento específico devem ser corrigidos imediatamente após a sua escrita. No caso de existirem razões razoáveis ​​para deixar tudo como está (por exemplo, se o código for escrito como algum tipo de espaço em branco ″ para mais tarde ″), esse fragmento deverá ser acompanhado por um comentário explicativo.

Também é importante notar que, independentemente das qualificações do desenvolvedor, alguns momentos problemáticos e simplesmente sem sucesso podem escapar do seu olhar e, às vezes, uma "solução temporária" pode ser aplicada em algum lugar, que logo será esquecido e se tornará "permanente". Posteriormente, a análise desse código (provavelmente outro desenvolvedor se envolverá nisso) exigirá um esforço inaceitável.

Nesses casos, a revisão de código pode ajudar. No entanto, se a tarefa for bastante séria, essa opção exigirá muito tempo. Além disso, quando existem muitos pequenos erros ou deficiências, o inspetor pode não perceber erros de alto nível por trás deles. A verificação do código se torna uma rotina tediosa e, gradualmente, a eficácia da revisão diminui.

Obviamente, as tarefas rotineiras serão muito melhor transferidas de pessoa para computador. Essa abordagem é usada em muitas áreas da modernidade. A automação de vários processos é a chave para a prosperidade. O que é automação no contexto deste tópico?

Um assistente confiável para resolver o problema de escrever código de trabalho razoável e estável é a análise estática. Cada vez antes de enviar os resultados de suas atividades para a revisão, o programador poderá realizar uma verificação automatizada e não sobrecarregar outros desenvolvedores (e ele próprio) com trabalho desnecessário. O código será enviado para verificação somente depois que todos os avisos do analisador forem levados em consideração: erros foram corrigidos e momentos estranhos foram reescritos, ou pelo menos explicados por um comentário.

Obviamente, a necessidade de revisão de código não desaparece, mas a análise estática complementa e simplifica bastante sua implementação. Uma parte suficientemente grande dos erros será corrigida graças ao analisador, e momentos estranhos definitivamente não serão esquecidos e marcados de acordo. Assim, com uma revisão de código, será possível focar na verificação da correção da implementação de interações lógicas complexas e na localização de problemas subjacentes que, infelizmente, não podem ser detectados pelo analisador (até o momento).

TensorFlow.NET




Este artigo foi inspirado no projeto TensorFlow.NET. Representa a implementação da capacidade de trabalhar com a funcionalidade da popular biblioteca de aprendizado de máquina TensorFlow por meio de código C # (a propósito, também testamos ). Essa ideia parecia bastante interessante, porque no momento da redação, o trabalho com a biblioteca estava disponível apenas em Python, Java e Go.

O código fonte disponível no GitHub está sendo constantemente atualizado e agora seu volume é um pouco mais de cem mil linhas. Após um estudo superficial, houve um desejo de realizar a verificação usando análise estática. O PVS-Studio foi usado como uma ferramenta específica, que provou sua eficácia em um número bastante grande de projetos diferentes .


Para o TensorFlow.NET, o analisador exibia vários alertas: 39 níveis altos, 227 níveis médios e 154 níveis baixos (você pode ler sobre os níveis de aviso aqui na subseção "Níveis de aviso e conjuntos de regras de diagnóstico"). Uma análise detalhada de cada um deles tornaria este artigo gigantesco; portanto, apenas os que me pareciam mais interessantes são descritos abaixo. Também é importante notar que alguns dos problemas encontrados são encontrados várias vezes no projeto, e a análise de cada parte desse código está além do escopo deste artigo.

O projeto define uma tarefa bastante séria e, infelizmente, a aparência de vários tipos de fragmentos de código estranhos é inevitável. Neste artigo, tentarei mostrar que o uso da análise estática pode simplificar bastante o trabalho dos programadores, apontando para áreas que podem causar perguntas. Nem sempre um aviso indica um erro, mas muitas vezes indica um código que causaria perguntas a uma pessoa. Consequentemente, a probabilidade de um código estranho ser redesenhado ou, em qualquer caso, comentado em conformidade é significativamente aumentada.

Fragmentos que atraíram a atenção ao estudar o relatório do analisador


De fato, um número razoavelmente grande de avisos do analisador para este projeto pode ser chamado de não muitos erros, mas um código estranho. Ao visualizar as linhas para as quais um aviso é emitido, surgem pelo menos confusão. Obviamente, alguns dos exemplos abaixo são provavelmente "soluções temporárias", mas não comentam esse problema, o que significa que alguém que trabalhará com esse código no futuro terá perguntas, procurando respostas para as quais levará tempo extra.


Ao mesmo tempo, alguns avisos apontam para um código que obviamente não é apenas estranho, mas apenas errado. Esse é o principal perigo do "código estranho" - é extremamente difícil perceber um erro real se aqui e ali você vê soluções incompreensíveis e se acostuma gradualmente ao fato de que o código parece estar incorreto.

Tour sofisticado de coleta


private static void _RemoveDefaultAttrs(....)
{
  var producer_op_dict = new Dictionary<string, OpDef>();
  producer_op_list.Op.Select(op =>
  {
    producer_op_dict[op.Name] = op;
    return op;
  }).ToArray();           
  ....
}

Aviso do analisador : V3010 O valor de retorno da função 'ToArray' deve ser utilizado. importer.cs 218

O analisador considera a chamada para Toray neste momento a ser suspeito, uma vez que o valor retornado por esta função não é atribuído a uma variável. No entanto, esse código não é um erro. Essa construção é usada para preencher o dicionário producer_op_dict com valores correspondentes aos elementos da lista producer_op_list.Op . Uma chamada para ToArray é necessária para que a função passada como argumento para o método Select seja chamada para todos os elementos da coleção.

Na minha opinião, o código não parece o melhor. O preenchimento do dicionário é um tanto óbvio, e alguns desenvolvedores podem querer remover a chamada "desnecessária" do ToArray . Seria muito mais simples e compreensível usar o loop foreach aqui :

var producer_op_dict = new Dictionary<string, OpDef>();

foreach (var op in producer_op_list.Op)
{
  producer_op_dict[op.Name] = op;
}

Nesse caso, o código parece o mais simples possível.

Outro momento semelhante é assim:

public GraphDef convert_variables_to_constants(....)
{
  ....
  inference_graph.Node.Select(x => map_name_to_node[x.Name] = x).ToArray();
  ....
}

Aviso do analisador : V3010 O valor de retorno da função 'ToArray' deve ser utilizado. graph_util_impl.cs 48

A única diferença é que esse registro parece mais conciso, mas apenas a tentação de remover a chamada do ToArray aqui não desaparece e ainda parece não óbvia.

Solução temporária


public GraphDef convert_variables_to_constants(....)
{
  ....
  var source_op_name = get_input_name(node);
  while(map_name_to_node[source_op_name].Op == "Identity")
  {
    throw new NotImplementedException);
    ....
  }
  ....
}

Aviso do analisador : V3020 Um 'lançamento' incondicional dentro de um loop. graph_util_impl.cs 73

No projeto em consideração, a seguinte abordagem é frequentemente usada: se algum comportamento precisar ser implementado posteriormente, uma NotImplementedException será lançada no local apropriado . Está claro por que o analisador alerta para um possível erro nesta peça: usar while em vez de if não parece muito razoável.

Este não é o único aviso que aparece devido ao uso de soluções temporárias. Por exemplo, existe um método:

public static Tensor[] _SoftmaxCrossEntropyWithLogitsGrad(
  Operation op, Tensor[] grads
)
{
  var grad_loss = grads[0];
  var grad_grad = grads[1];
  var softmax_grad = op.outputs[1];
  var grad = _BroadcastMul(grad_loss, softmax_grad);

  var logits = op.inputs[0];
  if(grad_grad != null && !IsZero(grad_grad)) // <=
  {
    throw new NotImplementedException("_SoftmaxCrossEntropyWithLogitsGrad");
  }

  return new Tensor[] 
  {
    grad,
    _BroadcastMul(grad_loss, -nn_ops.log_softmax(logits))
  };
}

Aviso do analisador : A expressão V3022 'grad_grad! = Null &&! IsZero (grad_grad)' é sempre falsa. nn_grad.cs 93

De fato, uma NotImplementedException ("_ SoftmaxCrossEntropyWithLogitsGrad") nunca será lançada, pois esse código é simplesmente inacessível. Para desvendar o motivo, você deve acessar o código da função IsZero :

private static bool IsZero(Tensor g)
{
  if (new string[] { "ZerosLike", "Zeros" }.Contains(g.op.type))
    return true;

  throw new NotImplementedException("IsZero");
}

O método retorna true ou lança uma exceção. Este código não é um erro - obviamente, a implementação aqui é deixada para mais tarde. O principal é que "então" realmente chegou. Bem, resultou com muito sucesso que o PVS-Studio não permitirá que você esqueça que existe uma imperfeição aqui :)

Tensor é Tensor?


private static Tensor[] _ExtractInputShapes(Tensor[] inputs)
{
  var sizes = new Tensor[inputs.Length];
  bool fully_known = true;
  for(int i = 0; i < inputs.Length; i++)
  {
    var x = inputs[i];

    var input_shape = array_ops.shape(x);
    if (!(input_shape is Tensor) || input_shape.op.type != "Const")
    {
      fully_known = false;
      break;
    }

    sizes[i] = input_shape;
  }
  ....
}

Aviso do analisador : V3051 Uma verificação de tipo excessiva. O objeto já é do tipo 'Tensor'. array_grad.cs 154

O tipo de retorno do método shape é Tensor . Portanto, a entrada input_shape é Tensor parece pelo menos estranha. Talvez, uma vez que o método retornou um valor de um tipo diferente e a verificação fizesse sentido, mas também é possível que, em vez de Tensor, a condição especifique algum tipo de herdeiro para essa classe. De uma forma ou de outra, o desenvolvedor deve prestar atenção a esse fragmento.

Verificação Completa


public static Tensor[] _BaseFusedBatchNormGrad(....)
{
  ....
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  var results = grad_fun(new FusedBatchNormParams
  {
    YBackprop = grad_y,
    X = x,
    Scale = scale,
    ReserveSpace1 = pop_mean,
    ReserveSpace2 = pop_var,
    ReserveSpace3 = version == 2 ? op.outputs[5] : null,
    Epsilon = epsilon,
    DataFormat = data_format,
    IsTraining = is_training
  });

  var (dx, dscale, doffset) = (results[0], results[1], results[2]);
  if (data_format == "NCHW") // <=
    throw new NotImplementedException("");

  ....
}

Avisos do analisador :

  • V3021 Existem duas instruções 'if' com expressões condicionais idênticas. A primeira instrução 'if' contém retorno de método. Isso significa que a segunda declaração 'if' não faz sentido nn_grad.cs 231
  • A expressão V3022 'data_format == "NCHW"' é sempre falsa. nn_grad.cs 247

Ao contrário de alguns dos exemplos anteriores, há claramente algo errado com o código. A segunda verificação não faz nenhum sentido, pois se a condição for verdadeira, a execução do programa não a alcançará. Talvez algum erro de digitação seja permitido aqui, ou uma das verificações geralmente seja supérflua.

A ilusão da escolha


public Tensor Activate(Tensor x, string name = null)
{
  ....
  Tensor negative_part;
  if (Math.Abs(_threshold) > 0.000001f)
  {
    negative_part = gen_ops.relu(-x + _threshold);
  } else
  {
    negative_part = gen_ops.relu(-x + _threshold);
  }
  ....
}

Aviso do analisador : V3004 A instrução 'then' é equivalente à instrução 'else'. gen_nn_ops.activations.cs 156

Uma demonstração bastante divertida da eficácia do uso da análise estática no desenvolvimento. É difícil encontrar uma razão razoável pela qual o desenvolvedor escreveu esse código específico - provavelmente esse é um erro típico de copiar e colar (embora esse, é claro, possa ser outro "para mais tarde").

Existem outros fragmentos de um plano semelhante, por exemplo:

private static Operation _GroupControlDeps(
  string dev, Operation[] deps, string name = null
)
{
  return tf_with(ops.control_dependencies(deps), ctl =>
  {
    if (dev == null)
    {
      return gen_control_flow_ops.no_op(name);
    }
    else
    {
      return gen_control_flow_ops.no_op(name);
    }
  });
}

Aviso do analisador : V3004 A instrução 'then' é equivalente à instrução 'else'. control_flow_ops.cs 135

Talvez uma vez que o sentido cheque, mas com o tempo ela desapareceu, ou algumas alterações adicionais estão previstas no futuro. No entanto, nenhuma dessas opções parece ser justificativa suficiente para deixar algo semelhante no código, sem explicar essa estranheza de forma alguma. Com um alto grau de probabilidade, um erro de copiar e colar foi feito exatamente da mesma maneira.

Cheque tardio


public static Tensor[] Input(int[] batch_shape = null,
  TF_DataType dtype = TF_DataType.DtInvalid,
  string name = null,
  bool sparse = false,
  Tensor tensor = null)
{
  var batch_size = batch_shape[0];
  var shape = batch_shape.Skip(1).ToArray(); // <=

  InputLayer input_layer = null;
  if (batch_shape != null)                   // <=
    ....
  else
    ....

  ....
}

Aviso do analisador : V3095 O objeto 'batch_shape' foi usado antes de ser verificado como nulo. Verifique as linhas: 39, 42. keras.layers.cs 39

Um erro clássico e bastante perigoso do uso potencial de uma variável, que é um link para lugar nenhum. Nesse caso, o código implica explicitamente a possibilidade de batch_shape ser nulo - isso fica claro na lista de argumentos e na verificação subsequente da mesma variável. Portanto, o analisador aqui indica um erro claro.

Outro ″ para depois ″?


public MnistDataSet(
  NDArray images, NDArray labels, Type dataType, bool reshape // <=
) 
{
  EpochsCompleted = 0;
  IndexInEpoch = 0;

  NumOfExamples = images.shape[0];

  images = images.reshape(
    images.shape[0], images.shape[1] * images.shape[2]
  );
  images = images.astype(dataType);
  // for debug np.multiply performance
  var sw = new Stopwatch();
  sw.Start();
  images = np.multiply(images, 1.0f / 255.0f);
  sw.Stop();
  Console.WriteLine($"{sw.ElapsedMilliseconds}ms");
  Data = images;

  labels = labels.astype(dataType);
  Labels = labels;
}

Aviso do analisador : O parâmetro do construtor V3117 'remodelar' não é usado. MnistDataSet.cs 15

Como algumas outras esquisitices, isso provavelmente ocorre devido ao fato de que a funcionalidade está longe de ser totalmente implementada e é bem possível que o parâmetro remodelar seja usado de alguma forma neste construtor no futuro. Mas, por enquanto, parece que ele está sendo jogado aqui exatamente assim. Se isso for realmente feito ″ para mais tarde ″, seria sensato marcar isso com algum comentário. Caso contrário, verifica-se que o código que constrói o objeto precisará lançar um parâmetro extra no construtor, e é bem possível que seria mais conveniente não fazer isso.

Possível dereferência nula indescritível


public static Tensor[] _GatherV2Grad(Operation op, Tensor[] grads)
{
  ....
  if((int)axis_static == 0)
  {
    var params_tail_shape = params_shape.slice(new NumSharp.Slice(start:1));
    var values_shape = array_ops.concat(
      new[] { indices_size, params_tail_shape }, 0
    );
    var values = array_ops.reshape(grad, values_shape);
    indices = array_ops.reshape(indices, indices_size);
    return new Tensor[]
    {
      new IndexedSlices(values, indices, params_shape), // <=
      null,
      null
    };
  }
  ....
}

Aviso do analisador : V3146 Possível desreferência nula do primeiro argumento 'valores' dentro do método. O '_outputs.FirstOrDefault ()' pode retornar o valor nulo padrão. array_grad.cs 199

Para entender qual é o problema, você deve primeiro recorrer ao código do construtor indexedSlices :

public IndexedSlices(
  Tensor values, Tensor indices, Tensor dense_shape = null
)
{
  _values = values;
  _indices = indices;
  _dense_shape = dense_shape;

  _values.Tag = this; // <=
}

Obviamente, passar nulo para esse construtor gerará uma exceção. No entanto, por que o analisador considera que a variável values pode conter nulo ?

O PVS-Studio usa a técnica de Análise de fluxo de dados, que permite encontrar os conjuntos de possíveis valores de variáveis ​​em diferentes partes do código. Um aviso informa que nulo na variável especificada pode ser retornado com a seguinte linha: _outputs.FirstOrDefault () . No entanto, observando o código acima, você pode descobrir que o valor da variável values é obtido chamando array_ops.reshape (grad, values_shape) . Então, onde é que _outputs.FirstOrDefault () então ?

O fato é que, ao analisar o fluxo de dados, não apenas a função atual é considerada, mas também todas chamadas; O PVS-Studio obtém informações sobre o conjunto de valores possíveis de qualquer variável em qualquer lugar. Portanto, um aviso significa que a implementação de array_ops.reshape (grad, values_shape) contém uma chamada para _outputs.FirstOrDefault () , cujo resultado é finalmente retornado.

Para verificar isso, vá para a implementação de remodelagem :

public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
            => gen_array_ops.reshape(tensor, shape, null);

Em seguida, vá para o método de reformulação chamado dentro:
public static Tensor reshape<T1, T2>(T1 tensor, T2 shape, string name = null)
{
  var _op = _op_def_lib._apply_op_helper(
    "Reshape", name, new { tensor, shape }
  );
  return _op.output;
}

A função _apply_op_helper retorna um objeto da classe Operation que contém a propriedade de saída . É após o recebimento de seu valor que o código descrito no aviso é chamado:

public Tensor output => _outputs.FirstOrDefault();

O tensor é, obviamente, um tipo de referência; portanto, o valor padrão para ele será nulo . Por tudo isso, pode-se ver que o PVS-Studio analisa meticulosamente a estrutura lógica do código, penetrando profundamente na estrutura das chamadas.

O analisador concluiu seu trabalho, indicando um local potencialmente problemático. O programador deve verificar se uma situação pode surgir quando elementos nos _outputs estão ausentes.

Portanto, a análise estática forçará pelo menos o desenvolvedor a prestar atenção ao fragmento suspeito, a fim de avaliar se o erro pode realmente ocorrer lá. Com essa abordagem, o número de erros que passam despercebidos será reduzido rapidamente.

Espera não confiável?


private (LoopVar<TItem>, Tensor[]) _BuildLoop<TItem>(
  ....
) where ....
{
  ....
  // Finds the closest enclosing non-None control pivot.
  var outer_context = _outer_context;
  object control_pivot = null;
  while (outer_context != null && control_pivot == null) // <=
  {

  }

  if (control_pivot != null)
  {

  }
  ....
}

Aviso do analisador : V3032 A espera dessa expressão não é confiável, pois o compilador pode otimizar algumas das variáveis. Use variáveis ​​voláteis ou primitivas de sincronização para evitar isso. WhileContext.cs 212

O analisador indica que essa implementação de espera pode ser otimizada pelo compilador, mas duvido que eles realmente tenham tentado implementar a espera aqui - provavelmente, o código simplesmente não foi adicionado e será desenvolvido no futuro. Pode valer a pena lançar uma NotImplementedException aqui , já que essa prática é usada em outras partes do projeto. De um jeito ou de outro, acredito que algum comentário explicativo obviamente não faria mal.

Quebrando fronteiras


public TensorShape(int[][] dims)
{
  if(dims.Length == 1)
  {
    switch (dims[0].Length)
    {
      case 0: shape = new Shape(new int[0]); break;
      case 1: shape = Shape.Vector((int)dims[0][0]); break;
      case 2: shape = Shape.Matrix(dims[0][0], dims[1][2]); break; // <=
      default: shape = new Shape(dims[0]); break;
    }
  }
  else
  {
    throw new NotImplementedException("TensorShape int[][] dims");
  }
}

Aviso do analisador : V3106 Possivelmente o índice está fora dos limites. O índice '1' está apontando além do limite 'dims'. TensorShape.cs 107

Entre os trechos estranhos de código que vi, houve um erro real, o que é muito difícil de notar. O fragmento a seguir está errado aqui: escurece [1] [2] . Obter um elemento com o índice 1 a partir de uma matriz de um elemento é obviamente um erro. Ao mesmo tempo, se você alterar o fragmento para dims [0] [2] , outro erro será exibido - obtendo um elemento com o índice 2 da matriz dims [0] , cujo comprimento nesse caso é ramificação 2. Portanto, esse problema acabou sendo como se estivesse com um "fundo duplo".

De qualquer forma, esse fragmento de código deve ser estudado e corrigido pelo desenvolvedor. Na minha opinião, este exemplo é uma excelente ilustração do desempenho da Análise de Fluxo de Dados no PVS-Studio.

Olepatka?


private void _init_from_args(object initial_value = null, ....) // <=
{
  var init_from_fn = initial_value.GetType().Name == "Func`1"; // <=
  ....
  tf_with(...., scope =>
  {
    ....
    tf_with(...., delegate
    {
      initial_value = ops.convert_to_tensor(  // <=
        init_from_fn ? (initial_value as Func<Tensor>)():initial_value,
        name: "initial_value",
        dtype: dtype
      );
    });
    _shape = shape ?? (initial_value as Tensor).TensorShape;
    _initial_value = initial_value as Tensor; // <=
    ....
    _dtype = _initial_value.dtype.as_base_dtype(); // <=

    if (_in_graph_mode)
    {
      ....

      if (initial_value != null) // <=
      {
        ....
      }

      ....
    }

    ....
  });
}

Para entender o código acima, também vale a pena trazer a implementação da função tf_with:

[DebuggerStepThrough] // with "Just My Code" enabled this lets the 
[DebuggerNonUserCode()]  //debugger break at the origin of the exception
public static void tf_with<T>(
  T py, Action<T> action
) where T : ITensorFlowObject
{
  try
  {
    py.__enter__();
    action(py);
  }
  finally
  {
    py.__exit__();
    py.Dispose();
  }
}

Aviso do analisador : V3019 Possivelmente, uma variável incorreta é comparada a nula após a conversão do tipo usando a palavra-chave 'as'. Verifique as variáveis ​​'valor_inicial', 'valor_inicial'. ResourceVariable.cs 137

_init_from_args é uma função bastante volumosa; muitos fragmentos foram omitidos. Você pode vê-lo completamente clicando no link . Embora o aviso no início não parecesse realmente sério para mim, depois de estudar, percebi que algo estava definitivamente errado com o código.

Primeiramente, deve-se notar que o método pode ser chamado sem passar parâmetros e, por padrão, será nulo no valor_inicial . Nesse caso, uma exceção será lançada na primeira linha. Em segundo lugar, a verificação

valor_inicial para a desigualdade nula parece estranho: se valor_inicial realmente se tornar nulo após chamar ops.convert_to_tensor , _initial_value seria nulo , o que significa que chamar _initial_value.dtype.as_base_dtype () também geraria uma exceção.

O analisador sugere que você pode precisar verificar nulo é _initial_value , mas, como observado acima, a referência a essa variável ocorre antes deste teste, portanto, essa opção também estaria incorreta.

Esse pequeno erro seria percebido em uma função tão gigante sem o PVS-Studio? Eu duvido muito.

Conclusão


Em um projeto com muitos exemplos de código estranho, muitos problemas podem ser ocultados. O programador, acostumado a ver o incompreensível, deixa de notar erros ao mesmo tempo. As consequências podem ser muito tristes. De fato, entre os avisos do analisador, também existem falsos; no entanto, na maioria dos casos, os avisos indicam pelo menos fragmentos de código que podem causar perguntas quando vistos por uma pessoa. No caso em que o código estranho é escrito intencionalmente, vale a pena deixar explicações para que o fragmento fique claro para o desenvolvedor que trabalhará com esse código no futuro (mesmo que isso signifique deixar comentários para si mesmo).

Ao mesmo tempo, ferramentas de análise estática, como o PVS-Studio, podem ser de grande ajuda para encontrar possíveis erros e esquisitices, para que sejam visíveis e não esquecidos, e todas as soluções temporárias sejam posteriormente refinadas e transformadas em um trabalho limpo, estruturado e estável o código.


Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Nikita Lipilin. Como o código estranho oculta erros? Análise TensorFlow.NET .

All Articles