From: Rusty Russell Date: Mon, 2 Jun 2008 02:16:51 +0000 (+1000) Subject: First primitive cut of ccanlint X-Git-Url: https://git.ozlabs.org/?p=ccan;a=commitdiff_plain;h=c8acddea39d222312102952e91c32cfe4dd2cea0 First primitive cut of ccanlint --- diff --git a/ccan_tools/ccanlint/Makefile b/ccan_tools/ccanlint/Makefile new file mode 100644 index 00000000..ddc5534b --- /dev/null +++ b/ccan_tools/ccanlint/Makefile @@ -0,0 +1,31 @@ +OBJS := ccan_tools/ccanlint/no_info.o \ + ccan_tools/ccanlint/has_main_header.o \ + ccan_tools/ccanlint/has_tests.o \ + ccan_tools/ccanlint/trailing_whitespace.o \ + ccan_tools/ccanlint/idempotent.o \ + +FUTURE:=ccan_tools/ccanlint/if_have_not_ifdef.o \ + ccan_tools/ccanlint/needs_depends.o \ + ccan_tools/ccanlint/has_info_documentation.o \ + ccan_tools/ccanlint/has_header_documentation.o \ + ccan_tools/ccanlint/has_tests.o \ + ccan_tools/ccanlint/builds_ok.o \ + ccan_tools/ccanlint/builds_ok_all_have_variants.o \ + ccan_tools/ccanlint/run_tests.o \ + ccan_tools/ccanlint/test_coverage.o \ + +ccan_tools/ccanlint/generated-init-tests: $(OBJS) + cat $(OBJS:.o=.c) | sed -n 's/^struct ccanlint \([A-Za-z0-9_]*\) = {/{ extern struct ccanlint \1; list_add(\&tests, \&\1.list); }/p' >$@ + +ccan_tools/ccanlint/ccanlint.o: ccan_tools/ccanlint/generated-init-tests + +ccan_tools/ccanlint/ccanlint: \ + $(OBJS) \ + ccan_tools/ccanlint/ccanlint.o \ + ccan_tools/ccanlint/get_file_lines.o \ + ccan_tools/ccanlint/file_analysis.o \ + talloc/talloc.o noerr/noerr.o + +ccanlint-clean: + $(RM) ccan_tools/ccanlint/generated-init-tests + diff --git a/ccan_tools/ccanlint/ccanlint.c b/ccan_tools/ccanlint/ccanlint.c new file mode 100644 index 00000000..001ee689 --- /dev/null +++ b/ccan_tools/ccanlint/ccanlint.c @@ -0,0 +1,165 @@ +/* + * ccanlint: assorted checks and advice for a ccan package + * Copyright (C) 2008 Rusty Russell + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include "ccanlint.h" +#include +#include +#include +#include +#include +#include +#include + +static unsigned int verbose = 0; +static LIST_HEAD(tests); + +static void init_tests(void) +{ +#include "generated-init-tests" +} + +static void usage(const char *name) +{ + fprintf(stderr, "Usage: %s [-s] [-v] [-d ]\n" + " -v: verbose mode\n" + " -s: simply give one line per FAIL and total score\n" + " -d: use this directory instead of the current one\n", + name); + exit(1); +} + +static void indent_print(const char *string) +{ + while (*string) { + unsigned int line = strcspn(string, "\n"); + printf("\t%.*s", line, string); + if (string[line] == '\n') { + printf("\n"); + line++; + } + string += line; + } +} + +bool ask(const char *question) +{ + char reply[2]; + + printf("%s ", question); + fflush(stdout); + + return fgets(reply, sizeof(reply), stdin) != NULL + && toupper(reply[0]) == 'Y'; +} + +static bool run_test(const struct ccanlint *i, + bool summary, + unsigned int *score, + unsigned int *total_score, + struct manifest *m) +{ + void *result; + unsigned int this_score; + + if (i->total_score) + *total_score += i->total_score; + + result = i->check(m); + if (!result) { + if (verbose) + printf(" %s: OK\n", i->name); + if (i->total_score) + *score += i->total_score; + return true; + } + + if (i->score) + this_score = i->score(m, result); + else + this_score = 0; + + *score += this_score; + if (summary) { + printf("%s FAILED (%u/%u)\n", + i->name, this_score, i->total_score); + + if (verbose) + indent_print(i->describe(m, result)); + return false; + } + + printf("%s\n", i->describe(m, result)); + + if (i->handle) + i->handle(m, result); + + return false; +} + +int main(int argc, char *argv[]) +{ + int c; + bool summary = false; + unsigned int score, total_score; + struct manifest *m; + const struct ccanlint *i; + + /* I'd love to use long options, but that's not standard. */ + /* FIXME: getopt_long ccan package? */ + while ((c = getopt(argc, argv, "sd:v")) != -1) { + switch (c) { + case 'd': + if (chdir(optarg) != 0) + err(1, "Changing into directory '%s'", optarg); + break; + case 's': + summary = true; + break; + case 'v': + verbose++; + break; + default: + usage(argv[0]); + } + } + + if (optind < argc) + usage(argv[0]); + + m = get_manifest(); + + init_tests(); + + /* If you don't pass the compulsory tests, you don't even get a score */ + if (verbose) + printf("Compulsory tests:\n"); + list_for_each(&tests, i, list) + if (!i->total_score && !run_test(i, summary, NULL, NULL, m)) + exit(1); + + if (verbose) + printf("\nNormal tests:\n"); + score = total_score = 0; + list_for_each(&tests, i, list) + if (i->total_score) + run_test(i, summary, &score, &total_score, m); + + printf("Total score: %u/%u\n", score, total_score); + + return 0; +} diff --git a/ccan_tools/ccanlint/ccanlint.h b/ccan_tools/ccanlint/ccanlint.h new file mode 100644 index 00000000..51f555ec --- /dev/null +++ b/ccan_tools/ccanlint/ccanlint.h @@ -0,0 +1,76 @@ +#ifndef CCAN_LINT_H +#define CCAN_LINT_H +#include +#include + +struct manifest { + char *basename; + struct ccan_file *info_file; + + struct list_head c_files; + struct list_head h_files; + + struct list_head run_tests; + struct list_head compile_ok_tests; + struct list_head compile_fail_tests; + struct list_head other_test_files; + + struct list_head other_files; +}; + +struct manifest *get_manifest(void); + +struct ccanlint { + struct list_node list; + + /* Unique name of test */ + const char *name; + + /* Total score that this test is worth. 0 means compulsory tests. */ + unsigned int total_score; + + /* If this returns non-NULL, it means the check failed. */ + void *(*check)(struct manifest *m); + + /* The non-NULL return from check is passed to one of these: */ + + /* So, what did this get out of the total_score? (NULL means 0). */ + unsigned int (*score)(struct manifest *m, void *check_result); + + /* Verbose description of what was wrong. */ + const char *(*describe)(struct manifest *m, void *check_result); + + /* Can we do something about it? (NULL if not) */ + void (*handle)(struct manifest *m, void *check_result); +}; + +/* Ask the user a yes/no question: the answer is NO if there's an error. */ +bool ask(const char *question); + +struct ccan_file { + struct list_node list; + + char *name; + + unsigned int num_lines; + char **lines; +}; + +/* Use this rather than accessing f->lines directly: loads on demand. */ +char **get_ccan_file_lines(struct ccan_file *f); + +/* Call the reporting on every line in the file. sofar contains + * previous results. */ +char *report_on_lines(struct list_head *files, + char *(*report)(const char *), + char *sofar); + +/* The critical tests which mean fail if they don't pass. */ +extern struct ccanlint no_info; +extern struct ccanlint has_main_header; + +/* Normal tests. */ +extern struct ccanlint trailing_whitespace; + + +#endif /* CCAN_LINT_H */ diff --git a/ccan_tools/ccanlint/file_analysis.c b/ccan_tools/ccanlint/file_analysis.c new file mode 100644 index 00000000..1341e57b --- /dev/null +++ b/ccan_tools/ccanlint/file_analysis.c @@ -0,0 +1,149 @@ +#include "ccanlint.h" +#include "get_file_lines.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +char **get_ccan_file_lines(struct ccan_file *f) +{ + if (!f->lines) + f->lines = get_file_lines(f, f->name, &f->num_lines); + return f->lines; +} + +static void add_files(struct manifest *m, const char *dir) +{ + DIR *d; + struct dirent *ent; + + if (dir[0]) + d = opendir(dir); + else + d = opendir("."); + if (!d) + err(1, "Opening directory %s", dir[0] ? dir : "."); + + while ((ent = readdir(d)) != NULL) { + struct stat st; + struct ccan_file *f; + struct list_head *dest; + bool is_c_src; + + if (ent->d_name[0] == '.') + continue; + + f = talloc(m, struct ccan_file); + f->lines = NULL; + f->name = talloc_asprintf(f, "%s%s", dir, ent->d_name); + if (lstat(f->name, &st) != 0) + err(1, "lstat %s", f->name); + + if (S_ISDIR(st.st_mode)) { + f->name = talloc_append_string(f->name, "/"); + add_files(m, f->name); + continue; + } + if (!S_ISREG(st.st_mode)) { + talloc_free(f); + continue; + } + + if (streq(f->name, "_info.c")) { + m->info_file = f; + continue; + } + + is_c_src = strends(f->name, ".c"); + if (!is_c_src && !strends(f->name, ".h")) + dest = &m->other_files; + else if (!strchr(f->name, '/')) { + if (is_c_src) + dest = &m->c_files; + else + dest = &m->h_files; + } else if (strstarts(f->name, "test/")) { + if (is_c_src) { + if (strstarts(f->name, "test/run")) + dest = &m->run_tests; + else if (strstarts(f->name, "test/compile_ok")) + dest = &m->compile_ok_tests; + else if (strstarts(f->name, "test/compile_fail")) + dest = &m->compile_fail_tests; + else + dest = &m->other_test_files; + } else + dest = &m->other_test_files; + } else + dest = &m->other_files; + + list_add(dest, &f->list); + } + closedir(d); +} + +char *report_on_lines(struct list_head *files, + char *(*report)(const char *), + char *sofar) +{ + struct ccan_file *f; + + list_for_each(files, f, list) { + unsigned int i; + char **lines = get_ccan_file_lines(f); + + for (i = 0; i < f->num_lines; i++) { + char *r = report(lines[i]); + if (!r) + continue; + + sofar = talloc_asprintf_append(sofar, + "%s:%u:%s\n", + f->name, i+1, r); + talloc_free(r); + } + } + return sofar; +} + +struct manifest *get_manifest(void) +{ + struct manifest *m = talloc(NULL, struct manifest); + unsigned int len; + + m->info_file = NULL; + list_head_init(&m->c_files); + list_head_init(&m->h_files); + list_head_init(&m->run_tests); + list_head_init(&m->compile_ok_tests); + list_head_init(&m->compile_fail_tests); + list_head_init(&m->other_test_files); + list_head_init(&m->other_files); + + /* *This* is why people hate C. */ + len = 32; + m->basename = talloc_array(m, char, len); + while (!getcwd(m->basename, len)) { + if (errno != ERANGE) + err(1, "Getting current directory"); + m->basename = talloc_realloc(m, m->basename, char, len *= 2); + } + + len = strlen(m->basename); + while (len && m->basename[len-1] == '/') + m->basename[--len] = '\0'; + + m->basename = strrchr(m->basename, '/'); + if (!m->basename) + errx(1, "I don't expect to be run from the root directory"); + m->basename++; + + add_files(m, ""); + return m; +} diff --git a/ccan_tools/ccanlint/get_file_lines.c b/ccan_tools/ccanlint/get_file_lines.c new file mode 100644 index 00000000..e9ef302b --- /dev/null +++ b/ccan_tools/ccanlint/get_file_lines.c @@ -0,0 +1,85 @@ +#include "get_file_lines.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void *grab_fd(const void *ctx, int fd) +{ + int ret; + unsigned int max = 16384, size = 0; + char *buffer; + + buffer = talloc_array(ctx, char, max+1); + while ((ret = read(fd, buffer + size, max - size)) > 0) { + size += ret; + if (size == max) + buffer = talloc_realloc(ctx, buffer, char, max*=2 + 1); + } + if (ret < 0) { + talloc_free(buffer); + buffer = NULL; + } else + buffer[size] = '\0'; + + return buffer; +} + +/* This version adds one byte (for nul term) */ +static void *grab_file(const void *ctx, const char *filename) +{ + int fd; + char *buffer; + + if (streq(filename, "-")) + fd = dup(STDIN_FILENO); + else + fd = open(filename, O_RDONLY, 0); + + if (fd < 0) + return NULL; + + buffer = grab_fd(ctx, fd); + close_noerr(fd); + return buffer; +} + +/* This is a dumb one which copies. We could mangle instead. */ +static char **split(const void *ctx, const char *text, const char *delims, + unsigned int *nump) +{ + char **lines = NULL; + unsigned int max = 64, num = 0; + + lines = talloc_array(ctx, char *, max+1); + + while (*text != '\0') { + unsigned int len = strcspn(text, delims); + lines[num] = talloc_array(lines, char, len + 1); + memcpy(lines[num], text, len); + lines[num][len] = '\0'; + text += len; + text += strspn(text, delims); + if (++num == max) + lines = talloc_realloc(ctx, lines, char *, max*=2 + 1); + } + lines[num] = NULL; + if (nump) + *nump = num; + return lines; +} + +char **get_file_lines(void *ctx, const char *name, unsigned int *num_lines) +{ + char *buffer = grab_file(ctx, name); + + if (!buffer) + err(1, "Getting file %s", name); + + return split(buffer, buffer, "\n", num_lines); +} diff --git a/ccan_tools/ccanlint/get_file_lines.h b/ccan_tools/ccanlint/get_file_lines.h new file mode 100644 index 00000000..2f8455a5 --- /dev/null +++ b/ccan_tools/ccanlint/get_file_lines.h @@ -0,0 +1,6 @@ +#ifndef GET_FILE_LINES_H +#define GET_FILE_LINES_H + +char **get_file_lines(void *ctx, const char *name, unsigned int *num_lines); + +#endif /* GET_FILE_LINES_H */ diff --git a/ccan_tools/ccanlint/has_main_header.c b/ccan_tools/ccanlint/has_main_header.c new file mode 100644 index 00000000..75e3f91d --- /dev/null +++ b/ccan_tools/ccanlint/has_main_header.c @@ -0,0 +1,41 @@ +#include "ccanlint.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void *check_has_main_header(struct manifest *m) +{ + struct ccan_file *f; + + list_for_each(&m->h_files, f, list) { + if (strstarts(f->name, m->basename) + && strlen(f->name) == strlen(m->basename) + 2) + return NULL; + } + return m; +} + +static const char *describe_has_main_header(struct manifest *m, + void *check_result) +{ + return talloc_asprintf(m, + "You have no %s/%s.h header file.\n\n" + "CCAN modules have a name, the same as the directory name. They're\n" + "expected to have an interface in the header of the same name.\n", + m->basename, m->basename); +} + +struct ccanlint has_main_header = { + .name = "No main header file", + .check = check_has_main_header, + .describe = describe_has_main_header, +}; diff --git a/ccan_tools/ccanlint/has_tests.c b/ccan_tools/ccanlint/has_tests.c new file mode 100644 index 00000000..7a187ab2 --- /dev/null +++ b/ccan_tools/ccanlint/has_tests.c @@ -0,0 +1,118 @@ +#include "ccanlint.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char test_is_not_dir[] = "test is not a directory"; + +static void *check_has_tests(struct manifest *m) +{ + struct stat st; + + if (lstat("test", &st) != 0) { + if (errno != ENOENT) + err(1, "statting test/"); + return "You have no test directory"; + } + + if (!S_ISDIR(st.st_mode)) + return test_is_not_dir; + + if (list_empty(&m->run_tests) && list_empty(&m->compile_ok_tests)) { + if (list_empty(&m->compile_fail_tests)) + return "You have no tests in the test directory"; + else + return "You have no positive tests in the test directory"; + } + return NULL; +} + +static const char *describe_has_tests(struct manifest *m, void *check_result) +{ + return talloc_asprintf(m, "%s\n\n" + "CCAN modules have a directory called test/ which contains tests.\n" + "There are three kinds of tests: run, compile_ok and compile_fail:\n" + "you can tell which type of test a C file is by its name, eg 'run.c'\n" + "and 'run-simple.c' are both run tests.\n\n" + "The simplest kind of test is a run test, which must compile with no\n" + "warnings, and then run: it is expected to use libtap to report its\n" + "results in a simple and portable format.\n" + "compile_ok tests are a subset of run tests: they must compile and\n" + "link, but aren't run.\n" + "compile_fail tests are tests which should fail to compile (or emit\n" + "warnings) or link when FAIL is defined, but should compile and link\n" + "when it's not defined: this helps ensure unrelated errors don't make\n" + "compilation fail.\n\n" + "Note that the tests are not linked against the files in the\n" + "above: you should directly #include those C files you want. This\n" + "allows access to static functions and use special effects inside\n" + "test files\n", (char *)check_result); +} + +static void handle_no_tests(struct manifest *m, void *check_result) +{ + FILE *run; + struct ccan_file *i; + + if (check_result == test_is_not_dir) + return; + + if (!ask("Should I create a template test/run.c file for you?")) + return; + + if (mkdir("test", 0600) != 0) { + if (errno != EEXIST) + err(1, "Creating test/ directory"); + } + + run = fopen("test/run.c", "w"); + if (!run) + err(1, "Trying to create a test/run.c"); + + fputs("/* Include the main header first, to test it works */\n", run); + fprintf(run, "#include \"%s/%s.h\"\n", m->basename, m->basename); + fputs("/* Include the C files directly. */\n", run); + list_for_each(&m->c_files, i, list) + fprintf(run, "#include \"%s/%s\"\n", m->basename, i->name); + fputs("#include \"tap/tap.h\"\n", run); + fputs("\n", run); + + fputs("int main(int argc, char *argv[])\n", run); + fputs("{\n", run); + fputs("\t/* This is how many tests you plan to run\n", run); + fputs("\tplan_tests(3);\n", run); + fputs("\n", run); + fputs("\t/* Simple thing we expect to succeed */\n", run); + fputs("\tok1(some_test())\n", run); + fputs("\t/* Same, with an explicit description of the test. */\n", run); + fputs("\tok(some_test(), \"%s with no args should return 1\", \"some_test\")\n", run); + fputs("\t/* How to print out messages for debugging. */\n", run); + fputs("\tdiag(\"Address of some_test is %p\", &some_test)\n", run); + fputs("\t/* Conditional tests must be explicitly skipped. */\n", run); + fputs("#if HAVE_SOME_FEATURE\n", run); + fputs("\tok1(test_some_feature())\n", run); + fputs("#else\n", run); + fputs("\tskip(1, \"Don\'t have SOME_FEATURE\")\n", run); + fputs("#endif\n", run); + fputs("\n", run); + fputs("\t/* This exits depending on whether all tests passed */\n", run); + fputs("\return exit_status()\n", run); + + fclose(run); +} + +struct ccanlint has_tests = { + .name = "No tests", + .check = check_has_tests, + .describe = describe_has_tests, + .handle = handle_no_tests, +}; diff --git a/ccan_tools/ccanlint/idempotent.c b/ccan_tools/ccanlint/idempotent.c new file mode 100644 index 00000000..44565575 --- /dev/null +++ b/ccan_tools/ccanlint/idempotent.c @@ -0,0 +1,71 @@ +#include "ccanlint.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char explain[] += "Headers usually start with the C preprocessor lines to prevent multiple\n" + "inclusions. These look like the following:\n" + "#ifndef MY_HEADER_H\n" + "#define MY_HEADER_H\n" + "...\n" + "#endif /* MY_HEADER_H */\n"; + +static char *report_idem(struct ccan_file *f, char *sofar) +{ + char **lines; + char *secondline; + + lines = get_ccan_file_lines(f); + if (f->num_lines < 3) + /* FIXME: We assume small headers probably uninteresting. */ + return NULL; + + if (!strstarts(lines[0], "#ifndef ")) + return talloc_asprintf_append(sofar, + "%s:1:expect first line to be #ifndef.\n", f->name); + + secondline = talloc_asprintf(f, "#define %s", + lines[0] + strlen("#ifndef ")); + if (!streq(lines[1], secondline)) + return talloc_asprintf_append(sofar, + "%s:2:expect second line to be '%s'.\n", + f->name, secondline); + + return sofar; +} + +static void *check_idempotent(struct manifest *m) +{ + struct ccan_file *f; + char *report = NULL; + + list_for_each(&m->h_files, f, list) + report = report_idem(f, report); + + return report; +} + +static const char *describe_idempotent(struct manifest *m, void *check_result) +{ + return talloc_asprintf(check_result, + "Some headers not idempotent:\n" + "%s\n%s", (char *)check_result, + explain); +} + +struct ccanlint idempotent = { + .name = "Headers are #ifndef/#define idempotent wrapped", + .total_score = 1, + .check = check_idempotent, + .describe = describe_idempotent, +}; diff --git a/ccan_tools/ccanlint/no_info.c b/ccan_tools/ccanlint/no_info.c new file mode 100644 index 00000000..9ee5f709 --- /dev/null +++ b/ccan_tools/ccanlint/no_info.c @@ -0,0 +1,78 @@ +#include "ccanlint.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void *check_no_info(struct manifest *m) +{ + if (m->info_file) + return NULL; + return m; +} + +static const char *describe_no_info(struct manifest *m, void *check_result) +{ + return "You have no _info.c file.\n\n" + "The file _info.c contains the metadata for a ccan package: things\n" + "like the dependencies, the documentation for the package as a whole\n" + "and license information.\n"; +} + +static const char template[] = + "#include \n" + "#include \"config.h\"\n" + "\n" + "/**\n" + " * %s - YOUR-ONE-LINE-DESCRIPTION-HERE\n" + " *\n" + " * This code ... YOUR-BRIEF-SUMMARY-HERE\n" + " *\n" + " * Example:\n" + " * FULLY-COMPILABLE-INDENTED-TRIVIAL-BUT-USEFUL-EXAMPLE-HERE\n" + " */\n" + "int main(int argc, char *argv[])\n" + "{\n" + " /* Expect exactly one argument\n" + " if (argc != 2)\n" + " return 1;\n" + "\n" + " if (strcmp(argv[1], \"depends\") == 0) {\n" + " PRINTF-CCAN-PACKAGES-YOU-NEED-ONE-PER-LINE-IF-ANY\n" + " return 0;\n" + " }\n" + "\n" + " return 1;\n" + "}\n"; + +static void create_info_template(struct manifest *m, void *check_result) +{ + FILE *info; + + if (!ask("Should I create a template _info.c file for you?")) + return; + + info = fopen("_info.c", "w"); + if (!info) + err(1, "Trying to create a template _info.c"); + + if (fprintf(info, template, m->basename) < 0) { + unlink_noerr("_info.c"); + err(1, "Writing template into _info.c"); + } + fclose(info); +} + +struct ccanlint no_info = { + .name = "No _info.c file", + .check = check_no_info, + .describe = describe_no_info, + .handle = create_info_template, +}; diff --git a/ccan_tools/ccanlint/trailing_whitespace.c b/ccan_tools/ccanlint/trailing_whitespace.c new file mode 100644 index 00000000..3aeec6e5 --- /dev/null +++ b/ccan_tools/ccanlint/trailing_whitespace.c @@ -0,0 +1,42 @@ +/* Trailing whitespace test. Almost embarrassing, but trivial. */ +#include "ccanlint.h" +#include +#include + +static char *report_on_trailing_whitespace(const char *line) +{ + if (!strends(line, " ") && !strends(line, "\t")) + return NULL; + + if (strlen(line) > 20) + return talloc_asprintf(line, "...'%s'", + line + strlen(line) - 20); + return talloc_asprintf(line, "'%s'", line); +} + +static void *check_trailing_whitespace(struct manifest *m) +{ + char *report; + + report = report_on_lines(&m->c_files, report_on_trailing_whitespace, + NULL); + report = report_on_lines(&m->h_files, report_on_trailing_whitespace, + report); + + return report; +} + +static const char *describe_trailing_whitespace(struct manifest *m, + void *check_result) +{ + return talloc_asprintf(check_result, + "Some source files have trailing whitespace:\n" + "%s", (char *)check_result); +} + +struct ccanlint trailing_whitespace = { + .name = "Lines with unnecessary trailing whitespace", + .total_score = 1, + .check = check_trailing_whitespace, + .describe = describe_trailing_whitespace, +};