Commit c8acddea authored by Rusty Russell's avatar Rusty Russell

First primitive cut of ccanlint

parent 89afb563
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
/*
* 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 <unistd.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#include <ctype.h>
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 <dirname>]\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;
}
#ifndef CCAN_LINT_H
#define CCAN_LINT_H
#include <list/list.h>
#include <stdbool.h>
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 */
#include "ccanlint.h"
#include "get_file_lines.h"
#include <talloc/talloc.h>
#include <string/string.h>
#include <noerr/noerr.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
#include <dirent.h>
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;
}
#include "get_file_lines.h"
#include <talloc/talloc.h>
#include <string/string.h>
#include <noerr/noerr.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <dirent.h>
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);
}
#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 */
#include "ccanlint.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <err.h>
#include <string/string.h>
#include <talloc/talloc.h>
#include <noerr/noerr.h>
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,
};
#include "ccanlint.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <err.h>
#include <string/string.h>
#include <talloc/talloc.h>
#include <noerr/noerr.h>
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,
};
#include "ccanlint.h"
#include <talloc/talloc.h>
#include <string/string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <err.h>
#include <string.h>
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,
};
#include "ccanlint.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <err.h>
#include <string.h>
#include <noerr/noerr.h>
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 <string.h>\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,
};
/* Trailing whitespace test. Almost embarrassing, but trivial. */
#include "ccanlint.h"
#include <talloc/talloc.h>
#include <string/string.h>
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,
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment