Analisador JSON simples em C

Aqui está um analisador JSON descendente recursivo simples, sem muitas funcionalidades extras, embora use a classe de vetor expansível revisada aqui ( Vetor expansível simples em C ). Não implementei nenhuma otimização, nem uma interface de acesso de nível superior, é tudo muito básico. Também não existe uma exportação JSON, apenas importação.

O código-fonte completo está disponível em github ( https://github.com/HarryDC/JsonParser ). CMake e código de teste incluídos, irei apenas postar os arquivos do analisador aqui.

Meu principal interesse seria se existem maneiras mais idiomáticas de escrever coisas em C. Mas é claro que qualquer outra contribuição é sempre apreciada também.

Cabeçalho

#ifndef HS_JSON_H #define HS_JSON_H #include "vector.h" enum json_value_type { TYPE_NULL, TYPE_BOOL, TYPE_NUMBER, TYPE_OBJECT, // Is a vector with pairwise entries, key, value TYPE_ARRAY, // Is a vector, all entries are plain TYPE_STRING, TYPE_KEY }; typedef struct { int type; union { int boolean; double number; char* string; char* key; vector array; vector object; } value; } json_value; // Parse string into structure of json elements and values // return 1 if successful. int json_parse(const char* input, json_value* root); // Free the structure and all the allocated values void json_free_value(json_value* val); // Convert value to string if possible, asserts if not char* json_value_to_string(json_value* value); // Convert value to double if possible asserts if not double json_value_to_double(json_value* value); // Convert value to bool if possible asserts if not int json_value_to_bool(json_value* value); // Convert value to vector if it"s an array asserts if not vector* json_value_to_array(json_value* value); // Convert value to vector if it"s an object, asserts if not vector* json_value_to_object(json_value* value); // Fetch the value with given index from root, asserts if root is not array json_value* json_value_at(const json_value* root, size_t index); // Fetche the value with the given key from root, asserts if root is not object json_value * json_value_with_key(const json_value * root, const char * key); #ifdef BUILD_TEST void json_test_all(void); #endif #endif 

Implementação

#include "json.h" #include <assert.h> #include <ctype.h> #include <stddef.h> #include <stdlib.h> #include <string.h> static int json_parse_value(const char ** cursor, json_value * parent); static void skip_whitespace(const char** cursor) { if (**cursor == "\0") return; while (iscntrl(**cursor) || isspace(**cursor)) ++(*cursor); } static int has_char(const char** cursor, char character) { skip_whitespace(cursor); int success = **cursor == character; if (success) ++(*cursor); return success; } static int json_parse_object(const char** cursor, json_value* parent) { json_value result = { .type = TYPE_OBJECT }; vector_init(&result.value.object, sizeof(json_value)); int success = 1; while (success && !has_char(cursor, "}")) { json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; } if (success) { *parent = result; } else { json_free_value(&result); } return success; return 1; } static int json_parse_array(const char** cursor, json_value* parent) { int success = 1; if (**cursor == "]") { ++(*cursor); return success; } while (success) { json_value new_value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &new_value); if (!success) break; skip_whitespace(cursor); vector_push_back(&parent->value.array, &new_value); skip_whitespace(cursor); if (has_char(cursor, "]")) break; else if (has_char(cursor, ",")) continue; else success = 0; } return success; } void json_free_value(json_value* val) { if (!val) return; switch (val->type) { case TYPE_STRING: free(val->value.string); val->value.string = NULL; break; case TYPE_ARRAY: case TYPE_OBJECT: vector_foreach(&(val->value.array), (void(*)(void*))json_free_value); vector_free(&(val->value.array)); break; } val->type = TYPE_NULL; } int json_is_literal(const char** cursor, const char* literal) { size_t cnt = strlen(literal); if (strncmp(*cursor, literal, cnt) == 0) { *cursor += cnt; return 1; } return 0; } static int json_parse_value(const char** cursor, json_value* parent) { // Eat whitespace int success = 0; skip_whitespace(cursor); switch (**cursor) { case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; case """: ++*cursor; const char* start = *cursor; char* end = strchr(*cursor, """); if (end) { size_t len = end - start; char* new_string = malloc((len + 1) * sizeof(char)); memcpy(new_string, start, len); new_string[len] = "\0"; assert(len == strlen(new_string)); parent->type = TYPE_STRING; parent->value.string = new_string; *cursor = end + 1; success = 1; } break; case "{": ++(*cursor); skip_whitespace(cursor); success = json_parse_object(cursor, parent); break; case "[": parent->type = TYPE_ARRAY; vector_init(&parent->value.array, sizeof(json_value)); ++(*cursor); skip_whitespace(cursor); success = json_parse_array(cursor, parent); if (!success) { vector_free(&parent->value.array); } break; case "t": { success = json_is_literal(cursor, "true"); if (success) { parent->type = TYPE_BOOL; parent->value.boolean = 1; } break; } case "f": { success = json_is_literal(cursor, "false"); if (success) { parent->type = TYPE_BOOL; parent->value.boolean = 0; } break; } case "n": success = json_is_literal(cursor, "null"); break; default: { char* end; double number = strtod(*cursor, &end); if (*cursor != end) { parent->type = TYPE_NUMBER; parent->value.number = number; *cursor = end; success = 1; } } } return success; } int json_parse(const char* input, json_value* result) { return json_parse_value(&input, result); } char* json_value_to_string(json_value* value) { assert(value->type == TYPE_STRING); return (char *)value->value.string; } double json_value_to_double(json_value* value) { assert(value->type == TYPE_NUMBER); return value->value.number; } int json_value_to_bool(json_value* value) { assert(value->type == TYPE_BOOL); return value->value.boolean; } vector* json_value_to_array(json_value* value) { assert(value->type == TYPE_ARRAY); return &value->value.array; } vector* json_value_to_object(json_value* value) { assert(value->type == TYPE_OBJECT); return &value->value.object; } json_value* json_value_at(const json_value* root, size_t index) { assert(root->type == TYPE_ARRAY); if (root->value.array.size < index) { return vector_get_checked(&root->value.array,index); } else { return NULL; } } json_value* json_value_with_key(const json_value* root, const char* key) { assert(root->type == TYPE_OBJECT); json_value* data = (json_value*)root->value.object.data; size_t size = root->value.object.size; for (size_t i = 0; i < size; i += 2) { if (strcmp(data[i].value.string, key) == 0) { return &data[i + 1]; } } return NULL; } #ifdef BUILD_TEST #include <stdio.h> void json_test_value_string(void) { printf("json_parse_value_string: "); // Normal parse, skip whitespace const char* string = " \n\t\"Hello World!\""; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_STRING); assert(result.value.string != NULL); assert(strlen(result.value.string) == 12); assert(strcmp("Hello World!", result.value.string) == 0); json_free_value(&result); // Empty string string = "\"\""; json_parse_value(&string, &result); assert(result.type == TYPE_STRING); assert(result.value.string != NULL); assert(strlen(result.value.string) == 0); json_free_value(&result); printf(" OK\n"); } void json_test_value_number(void) { printf("json_test_value_number: "); const char* string = " 23.4"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_NUMBER); assert(result.value.number == 23.4); json_free_value(&result); printf(" OK\n"); } void json_test_value_invalid(void) { printf("json_test_value_invalid: "); { // not a valid value const char* string = "xxx"; json_value result = { .type = TYPE_NULL }; assert(!json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } { // parse_value at end should fail const char* string = ""; json_value result = { .type = TYPE_NULL }; assert(!json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } printf(" OK\n"); } void json_test_value_array(void) { printf("json_test_value_array: "); { // Empty Array const char* string = "[]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 0); json_free_value(&result); } { // One Element const char* string = "[\"Hello World\"]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 1); json_value* string_value = (json_value *)result.value.array.data; assert(string_value->type == TYPE_STRING); assert(strcmp("Hello World", string_value->value.string) == 0);; json_free_value(&result); } { // Mutliple Elements const char* string = "[0, 1, 2, 3]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_ARRAY); assert(result.value.array.data != NULL); assert(result.value.array.size == 4); json_free_value(&result); } { // Failure const char* string = "[0, 2,,]"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(result.type == TYPE_NULL); assert(result.value.array.data == NULL); json_free_value(&result); } { // Failure // Shouldn"t need to free, valgrind shouldn"t show leak const char* string = "[0, 2, 0"; json_value result = { .type = TYPE_NULL }; assert(result.value.array.data == NULL); assert(result.type == TYPE_NULL); assert(result.value.array.data == NULL); } printf(" OK\n"); } void json_test_value_object(void) { printf("json_test_value_object: "); { // Empty Object const char* string = "{}"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 0); json_free_value(&result); } { // One Pair const char* string = "{ \"a\" : 1 }"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 2); json_value* members = (json_value *)result.value.object.data; assert(strcmp(json_value_to_string(members), "a") == 0); ++members; assert(json_value_to_double(members) == 1.0); json_free_value(&result); } { // Multiple Pairs const char* string = "{ \"a\": 1, \"b\" : 2, \"c\" : 3 }"; json_value result = { .type = TYPE_NULL }; assert(result.value.object.data == NULL); assert(json_parse_value(&string, &result)); assert(result.type = TYPE_OBJECT); assert(result.value.array.data != NULL); assert(result.value.array.size == 6); json_value* members = (json_value *)result.value.object.data; assert(strcmp(json_value_to_string(&members[4]), "c") == 0); assert(json_value_to_double(&members[5]) == 3.0); json_free_value(&result); } printf(" OK\n"); } void json_test_value_literal(void) { printf("json_test_values_literal: "); { const char* string = "true"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_BOOL); assert(result.value.boolean); json_free_value(&result); } { const char* string = "false"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_BOOL); assert(!result.value.boolean); json_free_value(&result); } { const char* string = "null"; json_value result = { .type = TYPE_NULL }; assert(json_parse_value(&string, &result)); assert(result.type == TYPE_NULL); json_free_value(&result); } printf(" OK\n"); } const char* test_string_valid = " \ { \"item1\" : [1, 2, 3, 4], \ \"item2\" : { \"a\" : 1, \"b\" : 2, \"c\" : 3 }, \ \"item3\" : \"An Item\" \ }"; const char* test_string_invalid = " \ { \"item1\" : [1, 2, 3, 4], \ \"item2\" : { \"a\" : 1, \"b\" : 2, \"c\" : 3 }, \ \"item3\" , \"An Item\" \ }"; void json_test_coarse(void) { printf("json_test_coarse: "); json_value root; assert(json_parse(test_string_valid, &root)); json_value* val = json_value_with_key(&root, "item1"); assert(root.type == TYPE_OBJECT); assert(root.value.object.size == 6); assert(val != NULL); assert(json_value_to_array(val) != NULL); assert(json_value_to_array(val)->size == 4); val = json_value_with_key(&root, "item3"); assert(val != NULL); assert(json_value_to_string(val) != NULL); assert(strcmp(json_value_to_string(val), "An Item") == 0); json_free_value(&root); // valgrind check for releasing intermediary data assert(!json_parse(test_string_invalid, &root)); printf(" OK\n"); } void json_test_all(void) { json_test_value_invalid(); json_test_value_string(); json_test_value_number(); json_test_value_array(); json_test_value_object(); json_test_value_literal(); json_test_coarse(); } #endif 

Resposta

Header

Em C, todos os enum nomes compartilham o mesmo namespace entre si ( e com coisas como nomes de variáveis). Portanto, é uma boa ideia tentar reduzir o risco de que eles colidam.

Seus enum json_value_type nomes têm o prefixo TYPE_, que é bastante genérico. Alguma outra biblioteca pode tentar usar o mesmo nome. Eu sugiro alterar esse prefixo para, digamos, JSON_.

Além disso, você não parece estar usando TYPE_KEY para qualquer coisa. Basta removê-lo.

Implementação

Como Roland Illig observa , os argumentos para iscntrl() e isspace() em sua skip_whitespace() função deve ser convertida para unsigned char para evite a extensão do sinal.

Como alternativa, e seguindo mais de perto a especificação JSON , você pode reescrever esta função simplesmente como:

static void skip_whitespace(const char** cursor) { while (**cursor == "\t" || **cursor == "\r" || **cursor == "\n" || **cursor == " ") ++(*cursor); } 

Muitas das suas static funções auxiliares fazem combinações não triviais de coisas e não têm nenhum comentário explicando o que elas Faz. Uma ou duas linhas de comentário antes de cada função podem ajudar muito na legibilidade.

Em particular, sua has_char() função faz um monte de coisas diferentes:

  1. Pula o espaço em branco.
  2. Ele verifica a presença de um determinado caractere na entrada.
  3. Se o caractere for encontrado, ele o pula automaticamente.

Apenas # 2 está obviamente implícito no nome da função; os outros são efeitos colaterais inesperados e devem pelo menos ser claramente documentados.

Na verdade, parece-me que seria melhor remover a chamada para skip_whitespace() de has_char() e apenas deixe o chamador pular explicitamente o espaço em branco antes de chamá-lo, se necessário. Em muitos casos, seu código já faz isso, tornando o salto duplicado redundante.

Além disso, para tornar o efeito # 3 menos surpreendente para o leitor, pode ser uma boa ideia renomear essa função para algo um pouco mais ativo como, digamos, read_char().


No final de json_parse_object(), você tem:

 return success; return 1; } 

Certamente isso é redundante. Basta se livrar do return 1;.

Além disso, parece que você está usando a função json_parse_value() genérica para analisar chaves de objetos e não testar para ter certeza de que são strings. Isso permite que algum JSON inválido chegue ao seu analisador. Eu sugiro adicionar uma verificação de tipo explícita ou dividir seu código de análise de string em uma função separada (conforme descrito abaixo) e chamá-lo diretamente de json_parse_object().


No topo de json_parse_array(), você tem:

if (**cursor == "]") { ++(*cursor); return success; } while (success) { 

Você pode reescrever isso da mesma maneira que você faz em json_parse_object():

while (success && !has_char("]")) { 

(Só que, você sabe, ainda acho que o nome read_char() seria melhor.)

Além disso, por algum motivo, seu json_parse_array() parece esperar que o chamador inicialize o parent struct, enquanto json_parse_object() faz isso automaticamente. AFAICT não há razão para a inconsistência, então você pode e provavelmente deve apenas fazer ambas as funções funcionarem da mesma forma.


Sua json_is_literal() função não está marcada como static, embora não aparecem no cabeçalho. Como is_char(), I “d al então prefira renomeá-lo para algo mais ativo, como json_read_literal() ou apenas read_literal(), para deixar mais claro que avança automaticamente o cursor em um correspondência bem-sucedida.

(Observe também que, conforme escrito, esta função não verifica se o literal na entrada realmente termina onde deveria. Por exemplo, corresponderia com sucesso a entrada nullnullnull contra null.Não acho que seja um bug real, já que os únicos literais válidos em JSON são true, false e null, nenhum dos quais são prefixos um do outro e uma vez que dois literais não podem aparecer consecutivamente em JSON válido sem algum outro token entre eles. Mas é definitivamente pelo menos digno de nota em um comentário.)


Você também pode querer marcar explicitamente algumas de suas funções auxiliares estáticas como inline para dar ao compilador uma dica de que ele deve tentar mesclá-los no código de chamada. Eu sugiro fazer isso pelo menos para skip_whitespace(), has_char() e json_is_literal().

Uma vez que suas json_value_to_X() funções de acessador consistem em nada além de uma assert() e uma desreferência de ponteiro, você também deve considerar mover suas implementações para json.h e marcá-las como static inline. Isso permitiria ao compilador embuti-los no código de chamada, mesmo em outros .c arquivos e, possivelmente, otimizar o assert() se o código já verifica o tipo de qualquer maneira.


Em sua função json_parse() principal, você pode querer verificar explicitamente se não há nada além de espaços em branco deixados no entrada após o valor raiz ter sido analisado.

Análise de string

Seu código de análise de string em json_parse_value() está quebrado, uma vez que não lidar com escapes de barra invertida. Por exemplo, ele falha na seguinte entrada JSON válida:

"I say: \"Hello, World!\"" 

Você pode querer adicionar isso como um caso de teste.

Você também deve testar se seu código lida corretamente com outras sequências de escape de barra invertida, como \b, \f, \n, \r, \t, \/ e especialmente \\ e \unnnn. Aqui estão mais alguns casos de teste para eles:

"\"\b\f\n\r\t\/\\" "void main(void) {\r\n\tprintf(\"I say: \\\"Hello, World!\\\"\\n\");\r\n}" "\u0048\u0065\u006C\u006C\u006F\u002C\u0020\u0057\u006F\u0072\u006C\u0064\u0021" "\u3053\u3093\u306B\u3061\u306F\u4E16\u754C" 

Visto que as strings JSON podem conter caracteres Unicode arbitrários, você precisará decidir como lidar com eles. Provavelmente, a escolha mais simples seria declarar sua entrada e saída em UTF-8 (ou talvez WTF-8 ) e converter \unnnn escapa em sequências de bytes UTF-8 (e, opcionalmente, vice-versa). Observe que, como você está usando strings terminadas em nulo, pode preferir decodificar \u0000 em a codificação excessivamente longa "\xC0\x80" em vez de um byte nulo.


Para manter a função json_parse_value() principal legível, Eu recomendo fortemente dividir o código de análise de string em uma função auxiliar separada. Especialmente porque fazê-lo lidar com escapes de barra invertida irá complicar consideravelmente.

Uma das complicações é que você não saberá realmente por quanto tempo o string será até que você a analise. Uma maneira de lidar com isso seria aumentar dinamicamente a string de saída alocada com realloc(), por exemplo:

// resize output buffer *buffer to new_size bytes // return 1 on success, 0 on failure static int resize_buffer(char** buffer, size_t new_size) { char *new_buffer = realloc(*buffer, new_size); if (new_buffer) { *buffer = new_buffer; return 1; } else return 0; } // parse a JSON string value // expects the cursor to point after the initial double quote // return 1 on success, 0 on failure static int json_parse_string(const char** cursor, json_value* parent) { int success = 1; size_t length = 0, allocated = 8; // start with an 8-byte buffer char *new_string = malloc(allocated); if (!new_string) return 0; while (success && **cursor != """) { if (**cursor == "\0") { success = 0; // unterminated string } // we"re going to need at least one more byte of space while (success && length + 1 > allocated) { success = resize_buffer(&new_string, allocated *= 2); } if (!success) break; if (**cursor != "\\") { new_string[length++] = **cursor; // just copy normal bytes to output ++(*cursor); } else switch ((*cursor)[1]) { case "\\":new_string[length++] = "\\"; *cursor += 2; break; case "/": new_string[length++] = "/"; *cursor += 2; break; case """: new_string[length++] = """; *cursor += 2; break; case "b": new_string[length++] = "\b"; *cursor += 2; break; case "f": new_string[length++] = "\f"; *cursor += 2; break; case "n": new_string[length++] = "\n"; *cursor += 2; break; case "r": new_string[length++] = "\r"; *cursor += 2; break; case "t": new_string[length++] = "\t"; *cursor += 2; break; case "u": // TODO: handle Unicode escapes! (decode to UTF-8?) // note that this may require extending the buffer further default: success = 0; break; // invalid escape sequence } } success = success && resize_buffer(&new_string, length+1); if (!success) { free(new_string); return 0; } new_string[length] = "\0"; parent->type = TYPE_STRING; parent->value.string = new_string; ++(*cursor); // move cursor after final double quote return 1; } 

Uma solução alternativa seria executar duas passagens de análise na entrada: uma apenas para determinar o comprimento da string de saída e outra para decodificá-la de fato. Isso seria feito mais facilmente algo assim:

static int json_parse_string(const char** cursor, json_value* parent) { char *tmp_cursor = *cursor; size_t length = (size_t)-1; if (!json_string_helper(&tmp_cursor, &length, NULL)) return 0; char *new_string = malloc(length); if (!new_string) return 0; if (!json_string_helper(&tmp_cursor, &length, new_string)) { free(new_string); return 0; } parent->type = TYPE_STRING; parent->value.string = new_string; *cursor = tmp_cursor; return 1; } 

onde a função auxiliar:

static int json_parse_helper(const char** cursor, size_t* length, char* new_string) { // ... } 

analisa uma string JSON de no máximo *length bytes em new_string e grava o comprimento real da string analisada em *length, ou, if new_string == NULL, apenas determina o comprimento da string sem realmente armazenar a saída decodificada em qualquer lugar.

Análise de número

Seu json_parse_value() a implementação trata os números como o caso padrão e simplesmente alimenta qualquer coisa que não esteja com ", [, {, n, t ou f na função de biblioteca padrão C strtod().

Visto que strtod() aceita um superconjunto de literais de número JSON válidos, isso deve funcionar, mas às vezes pode fazer seu código ac aceite JSON inválido como válido. Por exemplo, seu código aceitará +nan, -nan, +inf e -inf como números válidos e também aceitará notação hexadecimal como 0xABC123. Além disso, conforme a strtod() documentação vinculada acima, observa:

Em um local diferente do “C” padrão ou “POSIX” locales, esta função pode reconhecer sintaxe dependente de localidade adicional.

Se quiser ser mais rigoroso, convém validar explicitamente qualquer coisa que se pareça com um número em relação ao Gramática JSON antes de passá-la para strtod().

Observe também que strtod() pode definir errno por exemplo se o número de entrada estiver fora do intervalo de double. Você provavelmente deveria estar verificando isso.

Teste

Não olhei seus testes em detalhes, mas é ótimo ver que você os tem (mesmo que, conforme observado acima, sua cobertura poderia ser melhorada).

Pessoalmente, porém, eu prefiro mover os testes da implementação para um arquivo de origem separado. Isso tem vantagens e desvantagens:

  • A principal desvantagem é que você não pode mais testar funções auxiliares estáticas diretamente. No entanto, como sua API pública parece limpa e abrangente e não sofre de nenhum problema de “estado oculto” que complique o teste, você deve conseguir obter uma boa cobertura de teste mesmo apenas por meio da API.
  • A principal vantagem (além de uma separação clara entre o código de implementação e de teste) é que seus testes testarão automaticamente a API pública. Em particular, quaisquer problemas com o cabeçalho json.h aparecerão em seus testes. Além disso, fazer seus testes por meio da API ajuda a garantir que sua API seja realmente suficientemente completa e flexível para uso geral.

Se você realmente ainda deseja testar diretamente suas funções estáticas, você sempre pode adicionar um sinalizador de pré-processador que, opcionalmente, os expõe para teste, por meio de wrappers simples ou apenas removendo a static palavra-chave de suas definições.

Ps. I notei que seu json_test_value_number() teste está falhando para mim (GCC 5.4.0, i386 arch), provavelmente porque o número 23,4 não é exatamente representável em ponto flutuante. Alterá-lo para 23,5 torna o teste aprovado.

Comentários

  • Ótimo trabalho, obrigado, obviamente não ‘ t verificar o padrão tanto quanto eu deveria. Embora eu tenha desconsiderado utf-8 intencionalmente (provavelmente deveria ter mencionado isso.
  • OK, muito justo. Você provavelmente ainda deveria pelo menos decodificar escapes simples da forma \u00XX, já que alguns codificadores podem optar por usá-los até mesmo para caracteres ASCII. (Além disso, adicionei uma sugestão para marcar algumas de suas funções como inline acima, já que esqueci de fazer isso antes .)
  • Eu perdi totalmente essa parte do padrão, sim, as sequências de escape precisam ser analisadas corretamente.

Resposta

Esta não é de forma alguma uma revisão completa, mas compartilharei algumas coisas que chamaram minha atenção ao ler seu código.

Comentários

Enquanto comentários certamente são bons, alguns de seus comentários embutidos adicionam apenas ruído ao código.

// Eat whitespace int success = 0; skip_whitespace(cursor); 

Em primeiro lugar, o comentário está uma linha adiantado. Segundo, pode-se ler que o espaço em branco é consumido olhando para a função – o nome a descreve perfeitamente, o Não há necessidade de comentários adicionais.

case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; 

Novamente, este comentário apenas repete o que o próprio código está dizendo.


enum json_value_type { TYPE_NULL, TYPE_BOOL, TYPE_NUMBER, TYPE_OBJECT, // Is a vector with pairwise entries, key, value TYPE_ARRAY, // Is a vector, all entries are plain TYPE_STRING, TYPE_KEY }; 

Agora, esses comentários não são realmente inúteis, pois documentam o que cada valor representa. Mas por que apenas para TYPE_OBJECT e TYPE_ARRAY – por que não para todos os valores? Pessoalmente, eu colocaria um link para json.org pouco antes disso enum. Seus tipos são análogos a os que estão lá, você só precisa documentar o que TYPE_KEY deve ser. O que me leva ao próximo ponto …

TYPE_KEY

Dando uma olhada em json.org , você pode ver que um objeto consiste em uma lista de membros , que por sua vez são formados por uma string e um valor . O que significa que você realmente não precisa de TYPE_KEY! Basta adicionar uma nova estrutura para membros consistindo em um valor TYPE_STRING e outro valor json de qualquer tipo e você está pronto para começar. Agora, você pode tenha, por exemplo, um número como chave para um valor, o que não é permitido. Também tornaria parte da lógica relacionada ao objeto mais agradável, como este for loop:

for (size_t i = 0; i < size; i += 2) 

Ironicamente, a etapa deste loop for realmente poderia usar um comentário (por que += 2?), Mas não tem um.

Diversos

case "\0": // If parse_value is called with the cursor at the end of the string // that"s a failure success = 0; break; 

Por que não apenas return 0;?


while (iscntrl(**cursor) || isspace(**cursor)) ++(*cursor); 

e

if (success) ++(*cursor); 

e

if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; 

e alguns outros desses. Não sou adora colocar a condição e a instrução na mesma linha, especialmente porque você não está fazendo isso de forma consistente.Eu estou bem em fazer isso por uma questão de fluxo de controle, como if (!something) return;, mas ainda é “meh”. Melhor fazer direito e colocar a instrução em uma nova linha.


Além disso, acho que seu código poderia usar mais algumas linhas vazias para separar “regiões” ou como você gostaria de chamá-las . Por exemplo:

json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; 

Há uma linha em branco separando as coisas de configuração e análise das de verificação e devolução, mas você pode fazer melhor.

json_value key = { .type = TYPE_NULL }; json_value value = { .type = TYPE_NULL }; success = json_parse_value(cursor, &key); success = success && has_char(cursor, ":"); success = success && json_parse_value(cursor, &value); if (success) { vector_push_back(&result.value.object, &key); vector_push_back(&result.value.object, &value); } else { json_free_value(&key); break; } skip_whitespace(cursor); if (has_char(cursor, "}")) break; else if (has_char(cursor, ",")) continue; else success = 0; 

Acho isso muito mais limpo. Você tem um bloco para configurar os valores, um bloco para analisá-los, um bloco para colocá-los no vetor, um bloco para pular os espaços em branco e um bloco para finalizar a ação atual. A última linha vazia entre skip_whitespace(cursor); e if ... é discutível , mas eu prefiro assim.


Fora isso, descobri que seu código é facilmente legível e compreensível. Você verifica corretamente se há erros e usa uma nomenclatura sensata. Quanto à idiopatia, à parte pelo que mencionei, não há nada que eu marque como incomum ou não idiomático.

Resposta

As funções de ctype.h não devem ser chamadas com argumentos do tipo char, pois isso pode invocar um comportamento indefinido. Veja a documentação do NetBSD para uma boa explicação.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *