evil-qemacs/tools/scandoc.c
oguillaumin 509303a416 Improve documentation
- add @include directive
- add `match_annotation` for flexible matching, ignoring - and _
- add `fatal()`, `error()` and `warning()` handlers
2024-08-31 13:49:00 +02:00

698 lines
19 KiB
C

/*
* SCANDOC, Extract documentation from C source files
*
* Copyright (c) 2022-2024 Charlie Gordon.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/* A simple utility to preprocess C source code to extract
documentation comments. These comments start with an @ just after
the 2 initial characters of the comment.
They are ordered lexicographically.
The line starting with @ is removed from the output.
Macros are created with @MACRONAME=text till the enf of line
Macros are expanded before sorting the sections
*/
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NAME "scandoc"
#define VERSION "2024-05-20"
static int verbose;
__attribute__((format(printf, 2, 3)))
static void fatal(int err, const char *format, ...) {
va_list ap;
fprintf(stderr, "%s: ", NAME);
va_start(ap, format);
vfprintf(stderr, format, ap);
va_end(ap);
if (err)
fprintf(stderr, ": %s", strerror(err));
fprintf(stderr, "\n");
exit(1);
}
__attribute__((format(printf, 2, 3)))
static void error(int err, const char *format, ...) {
va_list ap;
fprintf(stderr, "%s: ", NAME);
va_start(ap, format);
vfprintf(stderr, format, ap);
va_end(ap);
if (err)
fprintf(stderr, ": %s", strerror(err));
fprintf(stderr, "\n");
}
__attribute__((format(printf, 1, 2)))
static void warning(const char *format, ...) {
va_list ap;
fprintf(stderr, "%s: ", NAME);
va_start(ap, format);
vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
}
static void *xmalloc(size_t size) {
void *p = malloc(size);
if (!p)
fatal(0, "cannot allocate memory");
return p;
}
static void *xrealloc(void *p, size_t size) {
p = realloc(p, size);
if (!p)
fatal(0, "cannot reallocate memory");
return p;
}
static int strstart(const char *str, const char *val, const char **ptr) {
size_t i;
for (i = 0; val[i] != '\0'; i++) {
if (str[i] != val[i])
return 0;
}
if (ptr)
*ptr = &str[i];
return 1;
}
static int match_annotation(const char *str, const char *val, const char **ptr) {
if (*str == '@')
str++;
while (*val) {
if (*str == '_' || *str == '-')
str++;
if (*str != *val)
return 0;
str++;
val++;
}
if (isalnum((unsigned char)*str))
return 0;
if (*str == ':')
str++;
while (isspace((unsigned char)*str))
str++;
if (ptr)
*ptr = str;
return 1;
}
static char *single_line(const char *s) {
/* convert the string `s` to a single line, replacing newlines and
adjacent white space with a single space and trimming initial and
trailing white space from the resulting string.
*/
char *res = strdup(s);
char *p = res;
char *q = res;
for (;;) {
while (isspace((unsigned char)*p))
p++;
while (*p && *p != '\n')
*q++ = *p++;
while (q > s && isspace((unsigned char)q[-1]))
*--q = '\0';
if (*p)
*q++ = ' ';
else
break;
}
*q = '\0';
return res;
}
typedef struct abbrev_t {
char *abbr;
size_t abbr_len;
char *rep;
size_t rep_len;
struct abbrev_t *next;
} abbrev_t;
static abbrev_t *abbrevs;
static abbrev_t *add_abbrev(const char *abbr, size_t abbr_len, const char *rep, size_t rep_len) {
abbrev_t *ap = xmalloc(sizeof *ap);
ap->abbr = strndup(abbr, abbr_len);
ap->abbr_len = strlen(ap->abbr);
ap->rep = strndup(rep, rep_len);
ap->rep_len = strlen(ap->rep);
ap->next = abbrevs;
return abbrevs = ap;
}
static abbrev_t *find_abbrev(const char *abbr, size_t abbr_len) {
abbrev_t *ap;
for (ap = abbrevs; ap; ap = ap->next) {
if (abbr_len == ap->abbr_len && !memcmp(abbr, ap->abbr, abbr_len))
break;
}
return ap;
}
typedef struct doc_t {
char *section;
char *text;
const char *filename;
int lineno;
} doc_t;
static doc_t *docs;
static int docs_len;
static int doc_strcmp(const char *p1, const char *p2) {
/* compare strings ignoring spaces and converting
numbers to implement natural ordering */
for (;;) {
unsigned char c1, c2;
while (isspace(c1 = *p1++))
continue;
while (isspace(c2 = *p2++))
continue;
if (isdigit(c1) && isdigit(c2)) {
long v1 = strtol(p1 - 1, (char **)(intptr_t)&p1, 10);
long v2 = strtol(p2 - 1, (char **)(intptr_t)&p2, 10);
if (v1 != v2)
return (v1 > v2) - (v2 > v1);
} else {
if (c1 != c2)
return c1 - c2;
if (c1 == '\0')
return 0;
}
}
}
static int doc_compare(const void *a1, const void *a2) {
const doc_t *dp1 = a1;
const doc_t *dp2 = a2;
int cmp = doc_strcmp(dp1->section, dp2->section);
return cmp ? cmp : doc_strcmp(dp1->text, dp2->text);
}
static int flush_docs(const char *outname) {
FILE *ft = stdout;
int i;
if (outname) {
if ((ft = fopen(outname, "w")) == NULL) {
error(errno, "cannot open output file %s", outname);
return 1;
}
}
qsort(docs, docs_len, sizeof(*docs), doc_compare);
for (i = 0; i < docs_len; i++) {
doc_t *dp = &docs[i];
char *p = dp->text;
int indent, len;
/* remove initial indentation spaces from each line */
indent = strspn(p, " ");
if (p[indent] == '\0')
continue;
if (verbose) {
//fprintf(ft, "\n[//] # (%s:%d:%s)\n",
// dp->filename, dp->lineno, dp->section);
fprintf(ft, "\n<!---\n%s:%d:%s\n-->\n",
dp->filename, dp->lineno, dp->section);
}
fprintf(ft, "\n");
for (;;) {
int ii = strspn(p, " ");
if (p[ii] == '\0')
break;
if (ii > indent)
ii = indent;
p += ii;
len = strcspn(p, "\n");
fprintf(ft, "%.*s\n", len, p);
if (p[len] == '\n')
len++;
p += len;
}
}
if (ft != stdout)
fclose(ft);
return 0;
}
static int rtrim(const char *s, int len) {
while (len > 0 && isspace((unsigned char)s[len - 1]))
--len;
return len;
}
static int skipwhite(const char *s) {
int pos = 0;
while (isspace((unsigned char)s[pos]))
pos++;
return pos;
}
static char *trimdup(const char *p) {
p += skipwhite(p);
return strndup(p, rtrim(p, strlen(p)));
}
static void freep(char **pp) {
free(*pp);
*pp = NULL;
}
static void concat(char **pp, const char *p2, int len2, int strip) {
char *p1 = *pp;
char *p3;
int len1, len3, space = 0;
len1 = p1 ? strlen(p1) : 0;
if (len2 < 0)
len2 = strlen(p2);
if (strip) {
len1 = rtrim(p1, len1);
while (len2 > 0 && isspace((unsigned char)*p2)) {
p2++;
len2--;
}
len2 = rtrim(p2, len2);
space = (len1 && len2);
}
len3 = len1 + space + len2;
p3 = xmalloc(len3 + 1);
memcpy(p3, p1, len1);
if (space)
p3[len1] = ' ';
memcpy(p3 + len1 + space, p2, len2);
p3[len3] = '\0';
freep(pp);
*pp = p3;
}
__attribute__((format(printf, 2, 0)))
static int vxprintf(char **strp, const char *format, va_list ap)
{
char buf[1024];
va_list arg;
char *str;
int len;
if (!strp || !format) {
errno = EINVAL;
return -1;
}
va_copy(arg, ap);
len = vsnprintf(buf, sizeof buf, format, arg);
va_end(arg);
if (len < 0 || (str = malloc(len + 1)) == NULL)
return -1;
if (len < (int)sizeof(buf)) {
memcpy(str, buf, len + 1);
} else {
vsnprintf(str, len + 1, format, ap);
}
free(*strp);
*strp = str;
return len;
}
__attribute__((format(printf, 2, 3)))
static int xprintf(char **strp, const char *format, ...)
{
va_list ap;
int len;
va_start(ap, format);
len = vxprintf(strp, format, ap);
va_end(ap);
return len;
}
static int xconcat(char **strp, size_t n, const char *s1,
const char *s2, const char *s3)
{
size_t len1 = strlen(s1);
size_t len2 = strlen(s2);
size_t len3 = strlen(s3);
size_t len = n + len1 + len2 + len3;
char *res = xmalloc(len + 1);
if (n)
memcpy(res, *strp, n);
memcpy(res + n, s1, len1);
memcpy(res + n + len1, s2, len2);
memcpy(res + n + len1 + len2, s3, len3 + 1);
free(*strp);
*strp = res;
return len;
}
static char *load_file(const char *filename) {
char *contents;
size_t size, len;
int c;
FILE *fp = fopen(filename, "r");
if (fp == NULL)
fatal(errno, "cannot include '%s'", filename);
contents = xmalloc(32);
size = 32;
len = 0;
while ((c = getc(fp)) != EOF) {
if (len + 1 > size) {
size += size / 2 + 32;
contents = xrealloc(contents, size);
}
contents[len++] = (char)c;
}
contents[len] = '\0';
fclose(fp);
return contents;
}
static void add_doc(const char *filename,
const char *comment, int lineno,
const char *proto_arg, int proto_line)
{
doc_t *dp;
char *sec;
char *text;
abbrev_t *ap;
int len = strlen(comment);
int line_len;
int abbr_len;
int text_len;
int sec_len;
if (len < 3 || comment[2] != '@')
return;
/* strip comment marks */
comment += 3;
len -= 3;
if (len > 1 && comment[len - 1] == '/' && comment[len - 2] == '*')
len -= 2;
/* strip boundary spaces */
while (len > 0 && *comment == ' ') {
comment++;
len--;
}
len = rtrim(comment, len);
for (line_len = 0; line_len < len && comment[line_len] != '\n'; line_len++)
continue;
/* extract section (first line) */
sec_len = rtrim(comment, line_len);
sec = strndup(comment, sec_len);
if (line_len < len && comment[line_len] == '\n')
line_len++;
text_len = len - line_len;
text = strndup(comment + line_len, text_len);
if (verbose) {
fprintf(stderr, "%s:%d:%s\n", filename, lineno, sec);
if (verbose > 1) {
fprintf(stderr, "%s\n", text);
}
}
/* scan the first word */
abbr_len = strcspn(sec, " =");
if (sec[abbr_len] == '=') {
/* macro definition */
char *rep = sec + abbr_len + 1;
/* store the expansion for the macro name */
ap = add_abbrev(sec, abbr_len, rep, strlen(rep));
/* remove the macro name and equal sign from the section line */
xprintf(&sec, "%s", rep);
} else
if ((ap = find_abbrev(sec, abbr_len)) != NULL) {
/* expand the macro inside in the section line */
xprintf(&sec, "%s%s", ap->rep, sec + abbr_len);
}
/* synthesize extra data based on tag */
if (ap) {
if (!strcmp(ap->abbr, "API") && proto_arg) {
int pos, epos, elen;
char *proto;
const char *p;
/* skip storage class from prototype */
strstart(proto_arg, "static ", &proto_arg);
strstart(proto_arg, "inline ", &proto_arg);
strstart(proto_arg, "extern ", &proto_arg);
/* linearize the prototype */
proto = single_line(proto_arg);
/* extract function name */
for (pos = epos = elen = 0; proto[pos]; pos++) {
if (proto[pos] == '(') {
elen = pos - epos;
break;
}
if (proto[pos] == ' ' || proto[pos] == '*') {
epos = pos + 1;
}
}
for (; proto[pos]; pos++) {
if (proto[pos] == ';' || proto[pos] == '{') {
break;
}
}
pos = rtrim(proto, pos);
/* entry name starts at epos, length is elen */
concat(&sec, proto + epos, elen, 1);
xprintf(&text, "%*s### `%.*s;`\n\n%s",
skipwhite(text), "", pos, proto, text);
/* TODO: should implement more elaborate text reformating */
/* TODO: handle function macros */
p = text;
while ((p = strchr(p, '@')) != NULL) {
int offset = p - text;
if (match_annotation(p, "argument", &p) || match_annotation(p, "param", &p)) {
xprintf(&text, "%.*s\n* argument %s", offset, text, p);
} else
if (match_annotation(p, "returns", &p) || match_annotation(p, "return", &p)) {
xprintf(&text, "%.*s\nReturn %s", offset, text, p);
} else
if (match_annotation(p, "note", &p)) {
xprintf(&text, "%.*s\nNote: %s", offset, text, p);
} else
if (match_annotation(p, "seealso", &p)) {
xprintf(&text, "%.*s\nSee also: %s", offset, text, p);
} else {
warning("unknown annotation: %s", p);
}
p = text + offset + 1;
}
free(proto);
} else {
const char *p = text;
while ((p = strchr(p, '@')) != NULL) {
int offset = p - text;
if (match_annotation(p, "include", &p)) {
char *fname = trimdup(p);
char *contents = load_file(fname);
xconcat(&text, offset, contents, "", "");
free(fname);
free(contents);
break;
} else {
warning("unknown annotation: %s", p);
}
}
}
}
/* append section */
docs = xrealloc(docs, (docs_len + 1) * sizeof(*docs));
dp = &docs[docs_len++];
dp->section = sec;
dp->text = text;
dp->filename = filename;
dp->lineno = lineno;
}
static int scandoc(const char *filename, FILE *fp) {
char buf[512];
int len, lineno = 0;
int in_comment = 0, comment_line = 0;
int in_proto = 0, proto_line = 0;
char *proto = NULL;
char *comment = NULL;
char *p;
while (fgets(buf, sizeof buf, fp)) {
if (in_proto == 2 && in_comment == 2) {
add_doc(filename, comment, comment_line, proto, proto_line);
freep(&comment);
freep(&proto);
in_comment = in_proto = 0;
}
lineno++;
p = buf + skipwhite(buf);
if (in_proto != 1) {
char *p1 = buf;
int p1_len;
if (!strncmp(p, "/*@", 3)) {
if (in_comment == 2) {
/* flush pending doc comment */
add_doc(filename, comment, comment_line, proto, proto_line);
freep(&comment);
freep(&proto);
in_comment = in_proto = 0;
}
freep(&comment);
comment_line = lineno;
in_comment = 1;
p1 = p;
}
p1_len = strlen(p1);
if (in_comment == 1) {
char *e = strstr(p1, "*/");
if (e) {
p1_len = e + 2 - p1;
in_comment = 2;
}
concat(&comment, p1, p1_len, 0);
continue;
}
if (!*p) {
/* blank line */
continue;
}
if (p == buf && *p != '/' && *p != '#' && *p != '}' && *p != '*') {
/* discard previous proto */
freep(&proto);
proto_line = lineno;
in_proto = 1;
}
}
if (in_proto == 1) {
if (!*p) {
/* blank line */
continue;
}
len = strcspn(p, ";{");
if (p[len] == '{' || p[len] == ';') {
in_proto = 2;
}
concat(&proto, p, len, 1);
}
}
if (in_comment == 2) {
add_doc(filename, comment, comment_line, proto, proto_line);
}
freep(&comment);
freep(&proto);
return 0;
}
int main(int argc, char *argv[]) {
char *filename = NULL;
char *outname = NULL;
int i;
int args_done = 0;
for (i = 1; i < argc; i++) {
char *arg = argv[i];
if (!args_done && *arg == '-') {
if (arg[1] == '\0') {
filename = arg;
scandoc("<stdin>", stdin);
continue;
}
if (arg[1] == '-') {
if (!strcmp(arg, "--")) {
args_done = 1;
} else
if (!strcmp(arg, "--help")) {
goto usage;
} else
if (!strcmp(arg, "--version")) {
printf("%s version %s\n", NAME, VERSION);
return 1;
} else {
warning("bad option: -%s", arg);
return 1;
}
}
while (*++arg) {
switch (*arg) {
case 'h':
case '?':
goto usage;
case 'o':
outname = arg[1] ? arg + 1 : argv[++i];
if (!outname) {
warning("missing filename for -o");
return 1;
}
break;
case 'v':
verbose++;
continue;
default:
warning("bad option: %s", arg);
return 1;
}
break;
}
} else {
FILE *fp;
filename = arg;
if ((fp = fopen(filename, "r")) == NULL) {
error(errno, "cannot open input file %s", filename);
return 1;
}
scandoc(filename, fp);
fclose(fp);
}
}
if (!filename) {
usage:
printf("usage: %s [-v] [-o FILENAME] FILE ...\n", NAME);
return 2;
}
/* output the documentation to outname or stdout */
flush_docs(outname);
return 0;
}