diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..09e6d59 --- /dev/null +++ b/.clang-format @@ -0,0 +1,18 @@ +--- +# fastwc style, codified. 4 spaces, Allman braces, 80 columns. +# The style guide is the law; clang-format is the enforcement. + +BasedOnStyle: LLVM +IndentWidth: 4 +ContinuationIndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 80 +BreakBeforeBraces: Allman +PointerAlignment: Right +DerivePointerAlignment: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +SortIncludes: CaseSensitive +IncludeBlocks: Regroup diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..2bf180f --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,14 @@ +--- +# fastwc clang-tidy configuration. +# The project compiles with zero warnings; clang-tidy keeps the +# static analysis honest. clang-analyzer-* runs in-editor via clangd. +# +# Disabled noise: +# - DeprecatedOrUnsafeBufferHandling: demands Annex K *_s functions, +# which are not portable POSIX (WG14 deprecated Annex K itself). +# - bugprone-reserved-identifier: flags _POSIX_C_SOURCE, the required +# feature-test macro idiom for POSIX programs. + +Checks: '-*,clang-analyzer-*,bugprone-*,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-bugprone-reserved-identifier,-bugprone-easily-swappable-parameters' +WarningsAsErrors: '' +FormatStyle: file diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..4602ec6 --- /dev/null +++ b/.clangd @@ -0,0 +1,11 @@ +# clangd configuration for fastwc. +# The style guide is the law; clangd is the enforcement. + +CompileFlags: + Compiler: clang + Add: + - -Wall + - -Wextra + +Diagnostics: + ClangTidy: true diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f5d3327 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# fastwc editor configuration — one file, one opinion, one indentation. + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[Makefile.am] +indent_style = tab + +[*.sh] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a1adb88 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# fastwc repository hygiene. + +* text=auto + +*.c text +*.h text +*.md text +*.sh text eol=lf +*.ac text +*.am text +*.json text + +*.c diff=cpp +*.h diff=cpp +*.sh diff=bash diff --git a/.gitignore b/.gitignore index 9566867..8c3d46f 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,8 @@ benchmarks/FAILED-benchmark.txt # ---> fastwc build artifacts bin/ fastwc +compile_commands.json +.cache/ # ---> autotools generated Makefile diff --git a/Makefile.am b/Makefile.am index 4abe191..5dcac04 100644 --- a/Makefile.am +++ b/Makefile.am @@ -12,7 +12,25 @@ release: all bench: release ./benchmarks/test-all.sh +# --- developer conveniences --- +# compile_commands.json for clangd (bear if present, else Makefile-derived). +compile-commands: + ./scripts/gen-compile-commands.sh + +# Make the code confess to the style guide. +format: + clang-format -i $(fastwc_SOURCES) + +# Verify the code already confesses, without touching it. +format-check: + clang-format --dry-run --Werror $(fastwc_SOURCES) + +# Static analysis via clang-tidy (needs compile_commands.json). +# Warnings are errors: the style guide is the law. +lint: compile-commands + clang-tidy -p . --warnings-as-errors='*' $(fastwc_SOURCES) + clean-local: rm -rf bin -.PHONY: release bench +.PHONY: release bench compile-commands format format-check lint diff --git a/README.md b/README.md index d963351..4047a69 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,26 @@ keep the minimum, and fail the moment fastwc loses a single case. GNU `wc` is used as an oracle the same way you'd use a broken clock: occasionally it's right, and it's the only one around. +## Development + +The editor setup is one command: + +```sh +make compile-commands # compile_commands.json for clangd +``` + +clangd reads `.clangd`, `.clang-tidy`, and `.clang-format` — the style +guide, enforced by robots. We use `bear` when it's installed; the +fallback hand-rolls the single entry from the Makefile, because one +source file doesn't need a database. + +- `make format` — make the code confess to the style guide +- `make format-check` — verify without touching +- `make lint` — clang-tidy, static analysis included + +`.editorconfig` and `.gitattributes` keep every editor honest. Your +editor has opinions. So do we. Ours are in the repo. + ## License MIT. Do whatever you want. We're not GNU, we won't sue you — we'll just diff --git a/scripts/gen-compile-commands.sh b/scripts/gen-compile-commands.sh new file mode 100755 index 0000000..98b0f2f --- /dev/null +++ b/scripts/gen-compile-commands.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env sh +# Generate compile_commands.json for clangd. +# +# Uses bear when available so every real compile command is captured +# (future-proof as the project grows). Otherwise hand-rolls the single +# entry from the flags in the generated Makefile — one source file, one +# entry, no mysteries. +set -eu + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" + +if [ ! -f Makefile ]; then + echo "gen-compile-commands: no Makefile found - run ./autogen.sh first" >&2 + exit 1 +fi + +if command -v bear >/dev/null 2>&1; then + bear -- make -B >/dev/null + # Drop the configure probes (conftest.c); clangd only wants real files. + jq 'map(select(.file | endswith("conftest.c") | not))' compile_commands.json \ + > compile_commands.json.tmp && mv compile_commands.json.tmp compile_commands.json + echo "compile_commands.json generated via bear" + exit 0 +fi + +CC=$(sed -n 's/^CC = //p' Makefile | head -n1) +[ -z "$CC" ] && CC=cc +DEFS=$(sed -n 's/^DEFS = //p' Makefile | head -n1) +AM_CFLAGS=$(sed -n 's/^AM_CFLAGS = //p' Makefile | head -n1) +CFLAGS=$(sed -n 's/^CFLAGS = //p' Makefile | head -n1) + +# Drop defines carrying embedded quotes (PACKAGE_* metadata): they would +# break JSON and clangd does not need them. +SAFE_DEFS='' +for d in $DEFS; do + case "$d" in + *\"*) ;; + *) SAFE_DEFS="$SAFE_DEFS $d" ;; + esac +done + +CMD="$CC$SAFE_DEFS -I. $AM_CFLAGS $CFLAGS -c src/main.c -o src/main.o" + +if command -v jq >/dev/null 2>&1; then + jq -n --arg d "$ROOT" --arg c "$CMD" --arg f "$ROOT/src/main.c" \ + '[{directory: $d, command: $c, file: $f}]' > compile_commands.json +else + printf '[{"directory":"%s","command":"%s","file":"%s"}]\n' \ + "$ROOT" "$CMD" "$ROOT/src/main.c" > compile_commands.json +fi +echo "compile_commands.json generated (fallback)" diff --git a/src/main.c b/src/main.c index e4f0264..1860462 100644 --- a/src/main.c +++ b/src/main.c @@ -19,24 +19,26 @@ #include #include -enum { - F_LINES = 1 << 0, /* -l: count '\n' */ - F_WORDS = 1 << 1, /* -w: whitespace-separated tokens */ - F_CHARS = 1 << 2, /* -m: multibyte characters */ - F_BYTES = 1 << 3, /* -c: bytes */ +enum +{ + F_LINES = 1 << 0, /* -l: count '\n' */ + F_WORDS = 1 << 1, /* -w: whitespace-separated tokens */ + F_CHARS = 1 << 2, /* -m: multibyte characters */ + F_BYTES = 1 << 3, /* -c: bytes */ }; static int flags = 0; -typedef struct { +typedef struct +{ long long lines; long long words; long long chars; long long bytes; - int ok; /* read succeeded */ + int ok; /* read succeeded */ } counts_t; -static unsigned char ws_tab[256]; /* ws_tab[c] = 1 if c is whitespace */ +static unsigned char ws_tab[256]; /* ws_tab[c] = 1 if c is whitespace */ static void init_ws_tab(void) { @@ -46,18 +48,17 @@ static void init_ws_tab(void) static void usage(FILE *out) { - fprintf(out, - "usage: fastwc [-lwc] [-m] [file...]\n" - "\n" - "Count lines, words, and bytes (default) or selected counts.\n" - "With no file, or when file is -, read standard input.\n" - "\n" - " -l count lines\n" - " -w count words\n" - " -c count bytes\n" - " -m count characters\n" - " --help display this help and exit\n" - " --version output version information and exit\n"); + fprintf(out, "usage: fastwc [-lwc] [-m] [file...]\n" + "\n" + "Count lines, words, and bytes (default) or selected counts.\n" + "With no file, or when file is -, read standard input.\n" + "\n" + " -l count lines\n" + " -w count words\n" + " -c count bytes\n" + " -m count characters\n" + " --help display this help and exit\n" + " --version output version information and exit\n"); } /* @@ -75,7 +76,8 @@ static long long count_newlines(const unsigned char *s, size_t n) long long k = 0; size_t i = 0; - for (; i + 8 <= n; i += 8) { + for (; i + 8 <= n; i += 8) + { uint64_t x; memcpy(&x, s + i, 8); x = (x ^ nl) & cl; @@ -99,7 +101,8 @@ static long long count_words(const unsigned char *s, size_t n, int *prev_ws) size_t i = 0; int prev = *prev_ws; - for (; i + 8 <= n; i += 8) { + for (; i + 8 <= n; i += 8) + { uint8_t m = 0; m |= (uint8_t)ws_tab[s[i + 0]] << 0; m |= (uint8_t)ws_tab[s[i + 1]] << 1; @@ -117,7 +120,8 @@ static long long count_words(const unsigned char *s, size_t n, int *prev_ws) prev = (m >> 7) & 1; } - for (; i < n; i++) { + for (; i < n; i++) + { int ws = ws_tab[s[i]]; if (prev && !ws) w++; @@ -142,27 +146,34 @@ static void count_stream_mb(FILE *fp, counts_t *c) memset(&st, 0, sizeof st); - while ((nread = fread(buf + pend, 1, sizeof buf - pend, fp)) > 0) { + while ((nread = fread(buf + pend, 1, sizeof buf - pend, fp)) > 0) + { size_t n = nread + pend; size_t i = 0; c->bytes += (long long)nread; - while (i < n) { + while (i < n) + { wchar_t wc; size_t r; - if (buf[i] < 0x80) { + if (buf[i] < 0x80) + { wc = buf[i]; r = 1; - } else { + } + else + { r = mbrtowc(&wc, (const char *)buf + i, n - i, &st); - if (r == (size_t)-2) { /* incomplete: carry over */ + if (r == (size_t)-2) + { /* incomplete: carry over */ pend = n - i; memmove(buf, buf + i, pend); break; } - if (r == (size_t)-1) { /* invalid sequence */ + if (r == (size_t)-1) + { /* invalid sequence */ memset(&st, 0, sizeof st); wc = L'\xfffd'; r = 1; @@ -171,9 +182,12 @@ static void count_stream_mb(FILE *fp, counts_t *c) if (wc == L'\n') c->lines++; - if (iswspace(wc)) { + if (iswspace(wc)) + { prev_ws = 1; - } else if (prev_ws) { + } + else if (prev_ws) + { c->words++; prev_ws = 0; } @@ -192,16 +206,21 @@ static void count_stream_mb(FILE *fp, counts_t *c) /* Fast path (-l/-w/-c): one pass, per-chunk memchr + table counting. */ static void count_stream(FILE *fp, counts_t *c) { - static unsigned char buf[1 << 17]; /* 128 KiB */ + static unsigned char buf[1 << 17]; /* 128 KiB */ size_t nread; - int prev_ws = 1; /* start of file: as if preceded by whitespace */ + int prev_ws = 1; /* start of file: as if preceded by whitespace */ - if (flags & F_CHARS) { + if (flags & F_CHARS) + { count_stream_mb(fp, c); return; } - while ((nread = fread(buf, 1, sizeof buf, fp)) > 0) { + for (;;) + { + nread = fread(buf, 1, sizeof buf, fp); /* NOLINT: EOF-state FP */ + if (nread == 0) + break; c->bytes += (long long)nread; if (flags & F_LINES) c->lines += count_newlines(buf, nread); @@ -217,38 +236,53 @@ static void count_file(const char *path, counts_t *c) { FILE *fp; - if (strcmp(path, "-") == 0) { - fp = stdin; - } else { - fp = fopen(path, "rb"); - if (fp == NULL) { - fprintf(stderr, "fastwc: %s: %s\n", path, strerror(errno)); - c->ok = 0; - return; - } + if (strcmp(path, "-") == 0) + { + count_stream(stdin, c); + if (ferror(stdin)) + fprintf(stderr, "fastwc: standard input: read error: %s\n", + strerror(errno)); + return; + } + + fp = fopen(path, "rb"); + if (fp == NULL) + { + fprintf(stderr, "fastwc: %s: %s\n", path, strerror(errno)); + c->ok = 0; + return; } count_stream(fp, c); if (ferror(fp)) - fprintf(stderr, "fastwc: %s: read error: %s\n", - strcmp(path, "-") == 0 ? "standard input" : path, - strerror(errno)); + fprintf(stderr, "fastwc: %s: read error: %s\n", path, strerror(errno)); - if (fp != stdin) - fclose(fp); + fclose(fp); } static int col_width(long long v) { int w = 1; - while (v >= 10) { + while (v >= 10) + { v /= 10; w++; } return w; } +static void widen(int *width, long long v, int enabled) +{ + int w; + + if (!enabled) + return; + w = col_width(v); + if (w > *width) + *width = w; +} + int main(int argc, char **argv) { counts_t *rows; @@ -258,29 +292,43 @@ int main(int argc, char **argv) init_ws_tab(); - for (a = 1; a < argc; a++) { + for (a = 1; a < argc; a++) + { const char *arg = argv[a]; if (arg[0] != '-' || arg[1] == '\0') - break; /* first file argument */ - if (strcmp(arg, "--") == 0) { + break; /* first file argument */ + if (strcmp(arg, "--") == 0) + { a++; break; } - if (strcmp(arg, "--help") == 0) { + if (strcmp(arg, "--help") == 0) + { usage(stdout); return 0; } - if (strcmp(arg, "--version") == 0) { + if (strcmp(arg, "--version") == 0) + { printf("fastwc 0.1.0\n"); return 0; } - for (const char *p = arg + 1; *p; p++) { - switch (*p) { - case 'l': flags |= F_LINES; break; - case 'w': flags |= F_WORDS; break; - case 'c': flags |= F_BYTES; break; - case 'm': flags |= F_CHARS; break; + for (const char *p = arg + 1; *p; p++) + { + switch (*p) + { + case 'l': + flags |= F_LINES; + break; + case 'w': + flags |= F_WORDS; + break; + case 'c': + flags |= F_BYTES; + break; + case 'm': + flags |= F_CHARS; + break; default: fprintf(stderr, "fastwc: invalid option -- '%c'\n", *p); usage(stderr); @@ -290,25 +338,30 @@ int main(int argc, char **argv) } if (flags == 0) - flags = F_LINES | F_WORDS | F_BYTES; /* wc default: -l -w -c */ + flags = F_LINES | F_WORDS | F_BYTES; /* wc default: -l -w -c */ if (flags & F_CHARS) setlocale(LC_CTYPE, ""); nfiles = argc - a; - if (nfiles == 0) { + if (nfiles == 0) + { rows = calloc(1, sizeof *rows); rows[0].ok = 1; count_stream(stdin, &rows[0]); - if (!rows[0].ok) { + if (!rows[0].ok) + { fprintf(stderr, "fastwc: standard input: read error: %s\n", strerror(errno)); failed = 1; } nfiles = 1; - } else { + } + else + { rows = calloc((size_t)nfiles, sizeof *rows); - for (i = 0; i < nfiles; i++) { + for (i = 0; i < nfiles; i++) + { rows[i].ok = 1; count_file(argv[a + i], &rows[i]); if (!rows[i].ok) @@ -320,36 +373,49 @@ int main(int argc, char **argv) int wl = 1, ww = 1, wm = 1, wb = 1; long long tl = 0, tw = 0, tm = 0, tb = 0; - for (i = 0; i < nfiles; i++) { + for (i = 0; i < nfiles; i++) + { counts_t *r = &rows[i]; - int x; - tl += r->lines; tw += r->words; tm += r->chars; tb += r->bytes; - if ((flags & F_LINES) && (x = col_width(r->lines)) > wl) wl = x; - if ((flags & F_WORDS) && (x = col_width(r->words)) > ww) ww = x; - if ((flags & F_CHARS) && (x = col_width(r->chars)) > wm) wm = x; - if ((flags & F_BYTES) && (x = col_width(r->bytes)) > wb) wb = x; + tl += r->lines; + tw += r->words; + tm += r->chars; + tb += r->bytes; + widen(&wl, r->lines, flags & F_LINES); + widen(&ww, r->words, flags & F_WORDS); + widen(&wm, r->chars, flags & F_CHARS); + widen(&wb, r->bytes, flags & F_BYTES); } - if ((flags & F_LINES) && col_width(tl) > wl) wl = col_width(tl); - if ((flags & F_WORDS) && col_width(tw) > ww) ww = col_width(tw); - if ((flags & F_CHARS) && col_width(tm) > wm) wm = col_width(tm); - if ((flags & F_BYTES) && col_width(tb) > wb) wb = col_width(tb); + widen(&wl, tl, flags & F_LINES); + widen(&ww, tw, flags & F_WORDS); + widen(&wm, tm, flags & F_CHARS); + widen(&wb, tb, flags & F_BYTES); - for (i = 0; i < nfiles; i++) { + for (i = 0; i < nfiles; i++) + { counts_t *r = &rows[i]; - if (flags & F_LINES) printf("%*lld ", wl, r->lines); - if (flags & F_WORDS) printf("%*lld ", ww, r->words); - if (flags & F_CHARS) printf("%*lld ", wm, r->chars); - if (flags & F_BYTES) printf("%*lld ", wb, r->bytes); + if (flags & F_LINES) + printf("%*lld ", wl, r->lines); + if (flags & F_WORDS) + printf("%*lld ", ww, r->words); + if (flags & F_CHARS) + printf("%*lld ", wm, r->chars); + if (flags & F_BYTES) + printf("%*lld ", wb, r->bytes); if (argc - a > 0) printf("%s", argv[a + i]); printf("\n"); } - if (argc - a > 1) { - if (flags & F_LINES) printf("%*lld ", wl, tl); - if (flags & F_WORDS) printf("%*lld ", ww, tw); - if (flags & F_CHARS) printf("%*lld ", wm, tm); - if (flags & F_BYTES) printf("%*lld ", wb, tb); + if (argc - a > 1) + { + if (flags & F_LINES) + printf("%*lld ", wl, tl); + if (flags & F_WORDS) + printf("%*lld ", ww, tw); + if (flags & F_CHARS) + printf("%*lld ", wm, tm); + if (flags & F_BYTES) + printf("%*lld ", wb, tb); printf("total\n"); }