
Convidamos você a tentar encontrar um erro em uma função muito simples do projeto GNU Midnight Commander. Porque Assim mesmo. É engraçado e interessante. Embora não, nós mentimos. Mais uma vez, queremos demonstrar um erro que uma pessoa encontra com dificuldade no processo de revisão de código, mas encontra facilmente o analisador de código estático do PVS-Studio.
Recentemente, recebemos uma carta perguntando por que o analisador gera um aviso na função
EatWhitespace , cujo código é fornecido abaixo. Na verdade, a questão não é tão simples. Tente descobrir o que há de errado com esse código.
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
Como você pode ver, a função
EatWhitespace é muito pequena. Mesmo um comentário em uma função ocupa mais espaço do que o corpo da própria função :). Agora alguns detalhes.
Descrição da função
Getc :
int getc ( FILE * stream );
A função retorna o caractere apontado pelo indicador interno da posição do arquivo do fluxo especificado. Então o indicador vai para o próximo caractere. Se o final do arquivo for atingido no momento da chamada para o fluxo, a função retornará
EOF e definirá o indicador de fim do arquivo para esse fluxo. Se ocorrer um erro de leitura, a função retornará um valor EOF e definirá um indicador de erro para o fluxo especificado (ferror).
Descrição da função
isspace :
int isspace( int ch );
A função verifica se o caractere está em branco, de acordo com a classificação do código do idioma atual. No código do idioma padrão, os seguintes caracteres são espaços em branco:
- espaço (0x20, ``);
- mudança de página (0x0c, '\ f');
- avanço de linha LF (0x0a, '\ n');
- retorno de carro CR (0x0d, '\ r');
- guia horizontal (0x09, '\ t');
- guia vertical (0x0b, '\ v').
Valor de retorno Valor diferente de zero, se o caractere for espaço em branco, zero caso contrário.
A função
EatWhitespace deve pular todos os caracteres considerados em branco, exceto o feed de linha '\ n'. Outro motivo para interromper a leitura de um arquivo pode estar chegando ao final do arquivo (EOF).
E agora, sabendo tudo isso, tente encontrar um erro!
Para impedir que o leitor acidentalmente não olhe imediatamente para a resposta, adicione alguns unicórnios em espera.

Figura 1. Hora de procurar um erro. Unicórnios vão esperar.Ainda não encontrou o erro?
O fato é que
enganamos os leitores sobre o
isspace . Haha Este não é um recurso padrão, mas uma macro caseira. Sim, somos inocentes e deixamos você confuso.

Figura 2. Um unicórnio dá aos leitores uma falsa impressão do que é isspace .Na verdade, é claro, nós e nosso unicórnio não temos culpa. Os autores do projeto GNU Midnight Commander contribuíram para a confusão ao decidir criar sua própria implementação de espaço de
iss no arquivo
charset.h :
#ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t')
Ao criar essa macro, alguns desenvolvedores confundiram outros. O código é escrito na suposição de que
isspace é uma função padrão que considera retornos de carro (0x0d, '\ r') como um dos caracteres de espaço em branco.
A macro implementada considera apenas espaços e tabulações como caracteres de espaço em branco. Vamos substituir a macro e ver o que acontece.
for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile))
A subexpressão ('\ n'! = C) é redundante (redundante), pois seu resultado sempre será verdadeiro. O analisador PVS-Studio alerta sobre isso, emitindo um aviso:
V560 Uma parte da expressão condicional é sempre verdadeira: ('\ n'! = C). params.c 136.
Para maior clareza, vamos analisar três opções para o desenvolvimento de eventos:
- O final do arquivo é atingido. O fim do arquivo (EOF) não é um espaço ou tabulação. A subexpressão ('\ n'! = C) não é calculada devido à avaliação de curto-circuito . O ciclo para.
- Qualquer caractere que não seja um espaço ou tabulação é lido. A subexpressão ('\ n'! = C) não é calculada devido à avaliação de curto-circuito. O ciclo para.
- Leia um caractere de espaço ou uma guia horizontal. A subexpressão ('\ n'! = C) é calculada, mas seu resultado sempre será verdadeiro.
Em outras palavras, o código revisado é equivalente a isso:
for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile))
Descobrimos que o código não funciona conforme o esperado. Vamos ver agora que consequências isso tem.
O programador que escreveu a chamada
isspace no corpo da função
EatWhitespace esperava que uma função padrão fosse chamada. Por isso, ele adicionou a condição de que o avanço de linha LF ('\ n') não deve ser considerado um caractere de espaço em branco.
Portanto, o programador planejou que, além das guias de espaço e horizontais, caracteres como mudança de página e guia vertical fossem ignorados.
Vale ressaltar que também foi planejado pular o caractere de retorno de carro CR (0x0d, '\ r'). Isso não acontece e o ciclo para quando encontra esse símbolo. Isso causará surpresas desagradáveis se o separador de linhas no arquivo for a sequência CR + LF usada em alguns sistemas não UNIX, como o Microsoft Windows.
Para aqueles que desejam aprender mais sobre os motivos históricos para usar LF ou CR + LF como separadores de linhas, aqui está o artigo da Wikipedia "
Feed de linha ".
A função
EatWhitespace deve processar arquivos da mesma maneira, onde LF e CR + LF são usados como um separador. Para o caso de CR + LF, não é assim. Em outras palavras, se seu arquivo veio do mundo do Windows, você está sem sorte :).
Talvez este não seja um erro sério, especialmente porque o GNU Midnight Commander é comum em sistemas operacionais do tipo UNIX, onde o caractere LF (0x0a, '\ n') é usado para converter uma linha. No entanto, devido a essas insignificâncias, surgem vários problemas irritantes de incompatibilidade de dados preparados nos sistemas Linux e Windows.
O erro descrito é interessante, pois é quase impossível detectar com uma revisão de código clássica. Nem todos os desenvolvedores de projetos podem conhecer os meandros da macro, e esquecê-los é muito fácil. Este é um bom exemplo de análise de código estática que complementa as análises de código e outras técnicas de detecção de erros.
Substituir funções padrão é uma má prática. A propósito, recentemente no artigo “
Análise de código estático do amor ”, um caso semelhante foi considerado com a macro
#define sprintf std :: printf .
Uma solução melhor seria atribuir à macro um nome exclusivo, por exemplo,
is_space_or_tab . Então a confusão seria impossível.
Talvez o motivo da criação da macro tenha sido a operação lenta da função
isspace padrão
, e o programador criou uma versão mais rápida, suficiente para resolver todas as tarefas necessárias. Mas ainda assim, esta decisão está errada. Seria mais
confiável definir o
isspace de maneira a obter código não compilado. E para implementar a funcionalidade necessária em uma macro com um nome exclusivo.
Obrigado pela atenção. Convidamos você a
baixar e experimentar o analisador PVS-Studio para testar seus projetos. Além disso, lembramos que recentemente o analisador adicionou suporte à linguagem Java.

Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Andrey Karpov.
Quer jogar um detetive? Encontre o bug em uma função do Midnight Commander .