├── .gitignore ├── LICENSE ├── Makefile ├── arg.h ├── common.h ├── config.mk ├── makefile.c ├── makel.c ├── test ├── tests ├── bad_ws.mk ├── comment_cont.mk ├── cont_of_blank.mk ├── cont_to_blank.mk ├── cont_without_ws.mk ├── eof_cont.mk ├── noninitial_bad_ws.mk ├── unindented_cont.mk └── ws_before_comment.mk ├── text.c ├── ui.c └── util.c /.gitignore: -------------------------------------------------------------------------------- 1 | *\#* 2 | *~ 3 | *.o 4 | *.a 5 | *.su 6 | *.gcov 7 | *.gcno 8 | *.gcda 9 | /makel 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | © 2021, 2022 Mattias Andrée 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | 3 | CONFIGFILE = config.mk 4 | include $(CONFIGFILE) 5 | 6 | OBJ =\ 7 | makel.o\ 8 | makefile.o\ 9 | text.o\ 10 | ui.o\ 11 | util.o 12 | 13 | HDR =\ 14 | arg.h\ 15 | common.h 16 | 17 | all: makel 18 | $(OBJ): $(HDR) 19 | 20 | .c.o: 21 | $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) 22 | 23 | makel: $(OBJ) 24 | $(CC) -o $@ $(OBJ) $(LDFLAGS) 25 | 26 | check: makel 27 | ./test 28 | 29 | install: makel 30 | mkdir -p -- "$(DESTDIR)$(PREFIX)/bin" 31 | mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" 32 | cp -- makel "$(DESTDIR)$(PREFIX)/bin/" 33 | cp -- makel.1 "$(DESTDIR)$(MANPREFIX)/man1/" 34 | 35 | uninstall: 36 | -rm -f -- "$(DESTDIR)$(PREFIX)/bin/makel" 37 | -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/makel.1" 38 | 39 | clean: 40 | -rm -f -- *.o *.a *.su *.gcov *.gcno *.gcda 41 | -rm -f -- makel 42 | 43 | .SUFFIXES: 44 | .SUFFIXES: .o .c 45 | 46 | .PHONY: all check install uninstall clean 47 | -------------------------------------------------------------------------------- /arg.h: -------------------------------------------------------------------------------- 1 | /* Trivial code, not subject to copyright, use as you see fit. 2 | * Reimplementation of 20h's arg.h */ 3 | 4 | #ifndef ARG_H 5 | #define ARG_H 6 | 7 | #include 8 | 9 | 10 | extern const char *argv0; 11 | 12 | 13 | #define ARGBEGIN do {\ 14 | char arg_h_flag_, arg_h_break_;\ 15 | if (!argc)\ 16 | break;\ 17 | argv0 = argv[0];\ 18 | while (--argc, *++argv && argv[0][0] == '-' && argv[0][1]) {\ 19 | if (argv[0][1] == '-' && !argv[0][2]) {\ 20 | argv++;\ 21 | argc--;\ 22 | break;\ 23 | }\ 24 | for (arg_h_break_ = 0; !arg_h_break_ && *++*argv;) {\ 25 | switch ((arg_h_flag_ = **argv)) 26 | 27 | #define ARGEND }\ 28 | }\ 29 | } while (0) 30 | 31 | 32 | #define FLAG() (arg_h_flag_) 33 | 34 | #define ARG() (arg_h_break_ = 1, argv[0][1] ? &argv[0][1] :\ 35 | argv[1] ? (argc--, *++argv) :\ 36 | (usage(), NULL)) 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "arg.h" 16 | 17 | 18 | #if defined(__GNUC__) 19 | # pragma GCC diagnostic ignored "-Wsuggest-attribute=format" 20 | # pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" 21 | # pragma GCC diagnostic ignored "-Wsuggest-attribute=pure" 22 | # pragma GCC diagnostic ignored "-Wsuggest-attribute=const" 23 | #endif 24 | 25 | 26 | #define EXIT_STYLE 1 27 | #define EXIT_CONFUSING 2 28 | #define EXIT_WARNING 3 29 | #define EXIT_UNSPECIFIED 4 30 | #define EXIT_NONCONFORMING 5 31 | #define EXIT_UNDEFINED 6 32 | #define EXIT_CRITICAL 7 33 | #define EXIT_ERROR 8 34 | 35 | 36 | #define ELEMSOF(ARRAY) (sizeof(ARRAY) / sizeof(*(ARRAY))) 37 | #define MAX(A, B) ((A) > (B) ? (A) : (B)) 38 | 39 | 40 | #define LIST_WARNING_CLASSES(X)\ 41 | X(WC_MAKEFILE, "makefile", INFORM)\ 42 | X(WC_EXTRA_MAKEFILE, "extra-makefile", WARN)\ 43 | X(WC_CMDLINE, "cmdline", WARN)\ 44 | X(WC_TEXT, "text", WARN)\ 45 | X(WC_ENCODING, "encoding", WARN)\ 46 | X(WC_LONG_LINE, "long-line", WARN_STYLE)\ 47 | X(WC_NONEMPTY_BLANK, "nonempty-blank", WARN_STYLE)\ 48 | X(WC_LEADING_BAD_SPACE, "leading-bad-space", WARN)\ 49 | X(WC_ILLEGAL_INDENT, "illegal-indent", WARN)\ 50 | X(WC_CONTINUATION_OF_BLANK, "continuation-of-blank", WARN)\ 51 | X(WC_CONTINUATION_TO_BLANK, "continuation-to-blank", WARN)\ 52 | X(WC_EOF_LINE_CONTINUATION, "eof-line-continuation", WARN)\ 53 | X(WC_UNINDENTED_CONTINUATION, "unindented-continuation", WARN)\ 54 | X(WC_SPACELESS_CONTINUATION, "spaceless-continuation", WARN)\ 55 | X(WC_COMMENT_CONTINUATION, "comment-continuation", WARN) 56 | 57 | 58 | enum action { 59 | IGNORE, 60 | INFORM, 61 | WARN_STYLE, 62 | WARN 63 | }; 64 | 65 | enum warning_class { 66 | #define X(ENUM, NAME, ACTION) ENUM, 67 | LIST_WARNING_CLASSES(X) 68 | #undef X 69 | NUM_WARNING_CLASS 70 | }; 71 | 72 | struct warning_class_data { 73 | const char *name; 74 | enum action action; 75 | }; 76 | 77 | enum line_class { 78 | EMPTY, /* Classified as comment lines in the specification */ 79 | BLANK, /* Classified as comment lines in the specification */ 80 | COMMENT, 81 | COMMAND_LINE, 82 | OTHER 83 | }; 84 | 85 | struct line { 86 | char *data; 87 | size_t len; 88 | const char *path; 89 | size_t lineno; 90 | int eof; 91 | int nest_level; 92 | char continuation_joiner; /* If '\\', it shall be '\\\n' */ 93 | }; 94 | 95 | enum macro_bracket_style { 96 | INCONSISTENT, 97 | ROUND, 98 | CURLY 99 | }; 100 | 101 | struct style { 102 | size_t max_line_length; 103 | int only_empty_blank_lines; 104 | enum macro_bracket_style macro_bracket_style; 105 | }; 106 | 107 | 108 | extern int exit_status; 109 | extern struct style style; 110 | 111 | 112 | /* makefile.c */ 113 | int open_default_makefile(const char **pathp); 114 | void cmdline_opt_f(const char *arg, const char **makefile_pathp); 115 | struct line *load_makefile(const char *path, size_t *nlinesp); 116 | 117 | 118 | /* text.c */ 119 | struct line *load_text_file(int fd, const char *fname, int nest_level, size_t *nlinesp); 120 | void check_utf8_encoding(struct line *line); 121 | void check_column_count(struct line *line); 122 | int is_line_blank(struct line *line); 123 | 124 | 125 | /* ui.c */ 126 | extern struct warning_class_data warning_classes[]; 127 | void xprintwarningf(enum warning_class class, int severity, const char *fmt, ...); 128 | #define warnf_style(CLASS, ...) xprintwarningf(CLASS, EXIT_STYLE, __VA_ARGS__) 129 | #define warnf_confusing(CLASS, ...) xprintwarningf(CLASS, EXIT_CONFUSING, __VA_ARGS__) 130 | #define warnf_warning(CLASS, ...) xprintwarningf(CLASS, EXIT_WARNING, __VA_ARGS__) 131 | #define warnf_unspecified(CLASS, ...) xprintwarningf(CLASS, EXIT_UNSPECIFIED, __VA_ARGS__) 132 | #define warnf_nonconforming(CLASS, ...) xprintwarningf(CLASS, EXIT_NONCONFORMING, __VA_ARGS__) 133 | #define warnf_undefined(CLASS, ...) xprintwarningf(CLASS, EXIT_UNDEFINED, __VA_ARGS__) 134 | void printinfof(enum warning_class class, const char *fmt, ...); 135 | void printerrorf(const char *fmt, ...); 136 | void printtipf(enum warning_class class, const char *fmt, ...); 137 | 138 | 139 | /* util.c */ 140 | void *erealloc(void *, size_t); 141 | void *ecalloc(size_t, size_t); 142 | void *emalloc(size_t); 143 | void *ememdup(const void *, size_t); 144 | void eprintf(const char *, ...); 145 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | PREFIX = /usr 2 | MANPREFIX = $(PREFIX)/share/man 3 | 4 | CC = c99 5 | 6 | CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE 7 | CFLAGS = -Wall -g 8 | LDFLAGS = -lgrapheme 9 | -------------------------------------------------------------------------------- /makefile.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #include "common.h" 3 | 4 | 5 | static const char *const default_makefiles[] = { 6 | "makefile", 7 | "Makefile" 8 | }; 9 | 10 | 11 | int 12 | open_default_makefile(const char **pathp) 13 | { 14 | int fd; 15 | size_t i; 16 | 17 | /* The specification says that the alternatives “shall be 18 | * tried”, but it uses the phrase “if neither … are found”, 19 | * implying that make(1) shall fail if a file cannot be 20 | * opened and only try the next alternative if it failed 21 | * becomes the file does not exist. 22 | */ 23 | 24 | for (i = 0; i < ELEMSOF(default_makefiles); i++) { 25 | *pathp = default_makefiles[i]; 26 | fd = open(*pathp, O_RDONLY); 27 | if (fd >= 0) { 28 | printinfof(WC_MAKEFILE, "found standard makefile to use: %s", *pathp); 29 | goto find_existing_fallbacks; 30 | } else if (errno != ENOENT) { 31 | eprintf("found standard makefile to use, but failed to open: %s:", *pathp); 32 | } 33 | } 34 | 35 | printerrorf("couldn't find any makefile to use, portable " 36 | "alternatives are ./makefile and ./Makefile"); 37 | 38 | find_existing_fallbacks: 39 | /* This serves two purposes: to inform the user that 40 | * we are only checking one of the files, and which 41 | * would (which is printed earlier), and to information 42 | * the user that it can be confusing. It is not common 43 | * practice run make(1) to generate ./makefile from 44 | * ./Makefile and (either immediately or by running 45 | * make(1) again) built the project from ./Makefile, 46 | * although that certainly can be useful if there are 47 | * parts of the makefile you want to generate, such 48 | * as .h file dependencies for .c files in very large 49 | * projects that have many .h files and many .c files 50 | * that each only depend on a few .h files. 51 | */ 52 | for (i++; i < ELEMSOF(default_makefiles); i++) 53 | if (!access(default_makefiles[i], F_OK)) 54 | warnf_confusing(WC_EXTRA_MAKEFILE, 55 | "found additional standard makefile, this be confusing: %s", 56 | default_makefiles[i]); 57 | 58 | return fd; 59 | } 60 | 61 | 62 | void 63 | cmdline_opt_f(const char *arg, const char **makefile_pathp) 64 | { 65 | static int warning_emitted = 0; 66 | 67 | if (*makefile_pathp && !warning_emitted) { 68 | warning_emitted = 1; 69 | warnf_unspecified(WC_CMDLINE, "the -f option has been specified multiple times, " 70 | "they are processed in order, but the behaviour is " 71 | "otherwise unspecified"); 72 | printinfof(WC_CMDLINE, "this implementation will use the last " 73 | "option and discard earlier options"); 74 | } 75 | 76 | *makefile_pathp = arg; 77 | } 78 | 79 | 80 | struct line * 81 | load_makefile(const char *path, size_t *nlinesp) 82 | { 83 | struct line *lines; 84 | int fd; 85 | 86 | if (!path) { 87 | fd = open_default_makefile(&path); 88 | } else if (!strcmp(path, "-")) { 89 | /* “A pathname of '-' shall denote the standard input” */ 90 | fd = dup(STDIN_FILENO); 91 | if (fd < 0) 92 | eprintf("dup :"); 93 | path = ""; 94 | } else { 95 | fd = open(path, O_RDONLY); 96 | if (fd < 0) 97 | eprintf("open %s O_RDONLY:", path); 98 | } 99 | 100 | lines = load_text_file(fd, path, 0, nlinesp); 101 | close(fd); 102 | return lines; 103 | } 104 | -------------------------------------------------------------------------------- /makel.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #include "common.h" 3 | 4 | const char *argv0 = "makel"; 5 | 6 | static void 7 | usage(void) { 8 | fprintf(stderr, "%s [-f makefile]\n", argv0); 9 | exit(EXIT_ERROR); 10 | } 11 | 12 | 13 | int exit_status = 0; 14 | 15 | struct style style = { 16 | .max_line_length = 120, 17 | .only_empty_blank_lines = 1, 18 | .macro_bracket_style = ROUND 19 | }; 20 | 21 | 22 | static void 23 | set_line_continuation_joiner(struct line *line) 24 | { 25 | if (line->len && line->data[line->len - 1] == '\\') { 26 | line->data[--line->len] = '\0'; 27 | /* Doesn't matter here if the first non-white space is # */ 28 | line->continuation_joiner = line->data[0] == '\t' ? '\\' : ' '; 29 | } else { 30 | line->continuation_joiner = '\0'; 31 | } 32 | } 33 | 34 | 35 | static void 36 | check_line_continuations(struct line *lines, size_t nlines) 37 | { 38 | size_t i, cont_from = 0; 39 | 40 | for (i = 0; i < nlines; i++) { 41 | set_line_continuation_joiner(&lines[i]); 42 | 43 | if (lines[i].continuation_joiner && 44 | (!i || !lines[i - 1].continuation_joiner) && 45 | is_line_blank(&lines[i])) { 46 | /* test cases: cont_of_blank.mk */ 47 | warnf_confusing(WC_CONTINUATION_OF_BLANK, 48 | "%s:%zu: initial line continuation on otherwise blank line, can cause confusion", 49 | lines[i].path, lines[i].lineno); 50 | } 51 | 52 | if (!lines[i].continuation_joiner && 53 | i && lines[i - 1].continuation_joiner && 54 | is_line_blank(&lines[i])) { 55 | /* test cases: cont_to_blank.mk */ 56 | warnf_confusing(WC_CONTINUATION_TO_BLANK, 57 | "%s:%zu: terminal line continuation to blank line, can cause confusion", 58 | lines[i].path, lines[i].lineno); 59 | } 60 | 61 | if (lines[i].continuation_joiner && lines[i].eof) { 62 | /* test cases: eof_cont.mk (TODO with lines[i].nest_level) */ 63 | warnf_unspecified(WC_EOF_LINE_CONTINUATION, 64 | "%s:%zu: line continuation at end of file, causes unspecified behaviour%s", 65 | lines[i].path, lines[i].lineno, 66 | !lines[i].nest_level ? "" : 67 | ", it is especially problematic in an included line"); 68 | printinfof(WC_EOF_LINE_CONTINUATION, "this implementation will remove the line continuation"); 69 | lines[i].continuation_joiner = 0; 70 | } 71 | 72 | if (i && lines[i - 1].continuation_joiner && lines[i].len) { 73 | if (!isspace(lines[i].data[0])) { 74 | if (lines[cont_from].len && !isspace(lines[cont_from].data[lines[cont_from].len - 1])) { 75 | /* test cases: cont_without_ws.mk (TODO with i != cont_from + 1) */ 76 | warnf_confusing(WC_SPACELESS_CONTINUATION, 77 | "%s:%zu,%zu: is proceeded by a non-white space " 78 | "character at the same time as the next line%s begins with " 79 | "a non-white space character, this can cause confusion as " 80 | "the make utility will add a whitespace", 81 | lines[cont_from].path, lines[cont_from].lineno, 82 | lines[i].lineno, i == cont_from + 1 ? "" : 83 | ", that consist of not only a ,"); 84 | } 85 | /* test cases: unindented_cont.mk, cont_without_ws.mk */ 86 | warnf_confusing(WC_UNINDENTED_CONTINUATION, 87 | "%s:%zu: continuation of line is not indented, can cause confusion", 88 | lines[i].path, lines[i].lineno); 89 | } 90 | cont_from = i; 91 | } else if (lines[i].continuation_joiner) { 92 | cont_from = i; 93 | } 94 | } 95 | } 96 | 97 | 98 | static enum line_class 99 | classify_line(struct line *line) 100 | { 101 | int warned_bad_space = 0; 102 | char *s; 103 | 104 | if (!line->len) 105 | return EMPTY; 106 | 107 | start_over: 108 | s = line->data; 109 | 110 | while (isspace(*s)) { 111 | if (!warned_bad_space && !isblank(*s)) { 112 | warned_bad_space = 1; 113 | /* test cases: bad_ws.mk, noninitial_bad_ws.mk */ 114 | warnf_undefined(WC_LEADING_BAD_SPACE, 115 | "%s:%zu: line contains leading white space other than " 116 | " and , which causes undefined behaviour", 117 | line->path, line->lineno); 118 | /* TODO what do we do here? */ 119 | } 120 | s++; 121 | } 122 | 123 | if (*s == '#') { 124 | if (line->data[0] != '#') { 125 | /* TODO should not apply if command line */ 126 | /* test cases: ws_before_comment.mk */ 127 | warnf_undefined(WC_ILLEGAL_INDENT, 128 | "%s:%zu: comment has leading white space, which is not legal", 129 | line->path, line->lineno); 130 | printinfof(WC_ILLEGAL_INDENT, "this implementation will recognise it as a comment line"); 131 | } 132 | return COMMENT; 133 | 134 | } else if (!*s) { 135 | if (line->continuation_joiner) { 136 | line++; 137 | goto start_over; 138 | } 139 | return BLANK; 140 | 141 | } else if (line->data[0] == '\t') { 142 | return COMMAND_LINE; 143 | 144 | } else { 145 | if (*s == '-') { /* We will warn about this later */ 146 | s++; 147 | while (isspace(*s)) 148 | s++; 149 | } 150 | 151 | /* TODO unspecified behaviour if include line with */ 152 | /* TODO unspecified behaviour if continuation that looks like an include line */ 153 | return OTHER; 154 | } 155 | } 156 | 157 | 158 | int 159 | main(int argc, char *argv[]) 160 | { 161 | const char *path = NULL; 162 | struct line *lines; 163 | size_t nlines; 164 | size_t i; 165 | 166 | /* make(1) shall support mixing of options and operands (up to --) */ 167 | ARGBEGIN { 168 | case 'f': 169 | cmdline_opt_f(ARG(), &path); 170 | break; 171 | 172 | default: 173 | usage(); 174 | } ARGEND; 175 | 176 | if (argc) 177 | usage(); 178 | 179 | setlocale(LC_ALL, ""); /* Required by wcwidth(3) */ 180 | 181 | lines = load_makefile(path, &nlines); 182 | 183 | for (i = 0; i < nlines; i++) { 184 | check_utf8_encoding(&lines[i]); 185 | check_column_count(&lines[i]); 186 | } 187 | 188 | check_line_continuations(lines, nlines); 189 | 190 | for (i = 0; i < nlines; i++) { 191 | switch (classify_line(&lines[i])) { 192 | case EMPTY: 193 | break; 194 | 195 | case BLANK: 196 | if (style.only_empty_blank_lines) { 197 | warnf_style(WC_NONEMPTY_BLANK, "%s:%zu: line is blank but not empty", /* TODO test cases */ 198 | lines[i].path, lines[i].lineno); 199 | } 200 | break; 201 | 202 | case COMMENT: 203 | break; 204 | 205 | case COMMAND_LINE: 206 | /* TODO list may, for historical reasons, end at a comment line; 207 | * note, the specifications specify “comment line” which is 208 | * define to include empty and blank lines; note however 209 | * that a line that begins with a that is prefixed 210 | * by whitespace is not a comment line, so, if it begins 211 | * with followed by zero or more whitespace, and then 212 | * a , it a command line, not a comment line. */ 213 | /* TODO on line continuation, remove first '\t', if any, and join with '\\\n' */ 214 | case OTHER: 215 | /* TODO first non-comment line shall be special target .POSIX without 216 | * prerequisites or commands, behaviour is unspecified otherwise */ 217 | /* TODO on line continuation, remove leading white space and join with ' ' */ 218 | break; 219 | 220 | default: 221 | abort(); 222 | } 223 | 224 | while (lines[i].continuation_joiner) { 225 | if (memchr(lines[i].data, '#', lines[i].len)) { /* TODO could also be a non-standard internal macro */ 226 | /* test cases: comment_cont.mk */ 227 | warnf_confusing(WC_COMMENT_CONTINUATION, 228 | "%s:%zu: using continuation of line to continue " 229 | "a comment on the next line can cause confusion", 230 | lines[i].path, lines[i].lineno); 231 | } 232 | i += 1; 233 | } 234 | /* TODO # in comment lines are very problematic. In make(1) 235 | * a comment can have a line continuation, but in sh(1) 236 | * comments cannot have line continuation. Furthermore, 237 | * any #, even if there is a be backslashed or in quotes, 238 | * becomes a comment; because of this, some implementations 239 | * of make do not recognise comments in command lines and 240 | * instead rely on sh(1) ignoring comments (this however 241 | * breaks POSIX compliance). */ 242 | /* TODO check if a # appears inside quotes or after a backslash */ 243 | } 244 | 245 | free(lines); 246 | return exit_status; 247 | } 248 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Every test file must start with a line formatted as follows: 4 | # #:: 5 | 6 | set -e 7 | exec >&2 8 | 9 | exit2str () { 10 | if test $1 = 0; then 11 | printf '%s\n' 'all good' 12 | else 13 | sed -n 's/^ *#define *EXIT_\([^ ]*\) *'"$1"' *$/ (\1)/p' < common.h | tr '[A-Z]' '[a-z]' 14 | fi 15 | } 16 | 17 | nfails=0 18 | 19 | for f in tests/*.mk; do 20 | header="$(head -n 1 < "$f")" 21 | expected=$(printf '%s' "$header" | cut -d : -f 2) 22 | options="$(printf '%s' "$header" | cut -d : -f 3-)" 23 | 24 | set +e 25 | ./makel -f "$f" $options >/dev/null 2>/dev/null 26 | got=$? 27 | set -e 28 | 29 | expstr="$(exit2str $expected)" 30 | gotstr="$(exit2str $got)" 31 | 32 | if test $got -lt $expected; then 33 | printf '%s: %s\n' "$f" "defect was not detected (expected ${expected}${expstr}, got ${got}${gotstr})" 34 | : $(( nfails++ )) 35 | elif test $got -gt $expected; then 36 | printf '%s: %s\n' "$f" "found more serious defects than expected (expected ${expected}${expstr}, got ${got}${gotstr})" 37 | : $(( nfails++ )) 38 | fi 39 | done 40 | 41 | if test $nfails -gt 0; then 42 | printf '%s\n' '----------' 43 | printf '%s\n' "${nfails} tests returned different exit codes than expected." 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /tests/bad_ws.mk: -------------------------------------------------------------------------------- 1 | #:6: 2 | foo: bar # This line is preceded by a vertical tab 3 | -------------------------------------------------------------------------------- /tests/comment_cont.mk: -------------------------------------------------------------------------------- 1 | #:2: 2 | #\ 3 | this line is a comment because of the continuation above 4 | -------------------------------------------------------------------------------- /tests/cont_of_blank.mk: -------------------------------------------------------------------------------- 1 | #:2: 2 | \ 3 | OBJS= 4 | -------------------------------------------------------------------------------- /tests/cont_to_blank.mk: -------------------------------------------------------------------------------- 1 | #:2: 2 | OBJS=\ 3 | 4 | -------------------------------------------------------------------------------- /tests/cont_without_ws.mk: -------------------------------------------------------------------------------- 1 | #:2: 2 | OBJ = make-will-actually\ 3 | insert-a-whitespace 4 | -------------------------------------------------------------------------------- /tests/eof_cont.mk: -------------------------------------------------------------------------------- 1 | #:4: 2 | # Continuation to end-of-file 3 | OBJS=\ 4 | -------------------------------------------------------------------------------- /tests/noninitial_bad_ws.mk: -------------------------------------------------------------------------------- 1 | #:6: 2 | foo: bar # This line is preceded by a vertical tab following a 3 | -------------------------------------------------------------------------------- /tests/unindented_cont.mk: -------------------------------------------------------------------------------- 1 | #:2: 2 | OBJ = \ 3 | obj.o 4 | -------------------------------------------------------------------------------- /tests/ws_before_comment.mk: -------------------------------------------------------------------------------- 1 | #:6: 2 | # This is not actually a valid comment 3 | -------------------------------------------------------------------------------- /text.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #include "common.h" 3 | 4 | 5 | static void 6 | print_long_line_tip(enum warning_class class) 7 | { 8 | printtipf(class, "you can put a at the end of the line to continue " 9 | "it on the next line, except in or immediately proceeding an " 10 | "include line"); 11 | } 12 | 13 | 14 | struct line * 15 | load_text_file(int fd, const char *fname, int nest_level, size_t *nlinesp) 16 | { 17 | struct line *lines; 18 | char *buf = NULL, *p; 19 | size_t size = 0; 20 | size_t len = 0; 21 | size_t i; 22 | ssize_t r; 23 | 24 | /* getline(3) may seem like the best way to read line by line, 25 | * however, it may terminate before the end of the line is 26 | * reached, which we would have to deal with, additionally, 27 | * we want to check for null bytes. Therefore we will keep 28 | * this simple and use read(3) and scan manually; and as a 29 | * bonus we can leave the file descriptor open, and let the 30 | * caller than created it close it. 31 | */ 32 | 33 | i = 0; 34 | *nlinesp = 0; 35 | for (;;) { 36 | if (len == size) 37 | buf = erealloc(buf, size += 2048); 38 | r = read(fd, &buf[len], size - len); 39 | if (r > 0) 40 | len += (size_t)r; 41 | else if (!r) 42 | break; 43 | else if (errno == EINTR) 44 | continue; 45 | else 46 | eprintf("read %s:", fname); 47 | 48 | for (; i < len; i++) { 49 | if (buf[i] == '\n') { 50 | *nlinesp += 1; 51 | buf[i] = '\0'; 52 | } else if (buf[i] == '\0') { 53 | /* https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_403 */ 54 | warnf_undefined(WC_TEXT, "%s:%zu: file contains a NUL byte, this is disallowed, because " 55 | "input files are text files, and causes undefined behaviour", 56 | fname, *nlinesp + 1); 57 | /* make(1) should probably just abort */ 58 | printinfof(WC_TEXT, "this implementation will replace it with a "); 59 | buf[i] = ' '; 60 | } 61 | } 62 | } 63 | 64 | if (len && buf[len - 1] != '\0') { /* LF has been converted to NUL above */ 65 | /* https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_403 */ 66 | warnf_undefined(WC_TEXT, "%s:%zu: is non-empty but does not end with a , which is " 67 | "required because input files are text files, and omission of it " 68 | "causes undefined behaviour", 69 | fname, *nlinesp + 1); 70 | /* make(1) should probably just abort */ 71 | printinfof(WC_TEXT, "this implementation will add the missing "); 72 | buf = erealloc(buf, len + 1); 73 | buf[len++] = '\0'; 74 | *nlinesp += 1; 75 | } 76 | 77 | lines = *nlinesp ? ecalloc(*nlinesp, sizeof(*lines)) : NULL; 78 | for (p = buf, i = 0; i < *nlinesp; i++) { 79 | lines[i].lineno = i + 1; 80 | lines[i].path = fname; 81 | lines[i].len = strlen(p); 82 | lines[i].data = ememdup(p, lines[i].len + 1); 83 | lines[i].eof = i + 1 == *nlinesp; 84 | lines[i].nest_level = nest_level; 85 | 86 | if (lines[i].len + 1 > 2048) { 87 | /* https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_403 */ 88 | warnf_undefined(WC_TEXT, "%s:%zu: line is, including the character, longer than " 89 | "2048 bytes which causes undefined behaviour as input files are " 90 | "text files and POSIX only guarantees support for lines up to 2048 " 91 | "bytes long including the character in text files", 92 | fname, *nlinesp + 1); 93 | printinfof(WC_TEXT, "this implementation supports arbitrarily long lines"); 94 | print_long_line_tip(WC_TEXT); 95 | } 96 | p += lines[i].len + 1; 97 | } 98 | 99 | free(buf); 100 | return lines; 101 | } 102 | 103 | 104 | void 105 | check_utf8_encoding(struct line *line) 106 | { 107 | size_t off, r; 108 | uint_least32_t codepoint; 109 | #if GRAPHEME_INVALID_CODEPOINT == 0xFFFD 110 | unsigned char invalid_codepoint_encoding[] = {0xEF, 0xBF, 0xBD}; 111 | #endif 112 | 113 | for (off = 0; off < line->len; off += r) { 114 | r = grapheme_decode_utf8(&line->data[off], line->len - off, &codepoint); 115 | 116 | if (codepoint == GRAPHEME_INVALID_CODEPOINT && 117 | (r != ELEMSOF(invalid_codepoint_encoding) || 118 | memcmp(&line->data[off], invalid_codepoint_encoding, r))) { 119 | 120 | warnf_unspecified(WC_ENCODING, "%s:%zu: line contains invalid UTF-8", line->path, line->lineno); 121 | printinfof(WC_ENCODING, "this implementation will replace it the " 122 | "Unicode replacement character (U+FFFD)"); 123 | 124 | line->data = erealloc(line->data, line->len - r + ELEMSOF(invalid_codepoint_encoding)); 125 | memmove(&line->data[off + ELEMSOF(invalid_codepoint_encoding)], 126 | &line->data[off + r], 127 | line->len - off - r); 128 | memcpy(&line->data[off], invalid_codepoint_encoding, ELEMSOF(invalid_codepoint_encoding)); 129 | line->len -= r; 130 | line->len += r = ELEMSOF(invalid_codepoint_encoding); 131 | } 132 | } 133 | } 134 | 135 | 136 | void 137 | check_column_count(struct line *line) 138 | { 139 | size_t columns = 0; 140 | size_t off, r; 141 | uint_least32_t codepoint; 142 | 143 | if (line->len <= style.max_line_length) /* Column count cannot be more than byte count */ 144 | return; 145 | 146 | for (off = 0; off < line->len; off += r) { 147 | r = grapheme_decode_utf8(&line->data[off], line->len - off, &codepoint); 148 | columns += (size_t)abs(wcwidth((wchar_t)codepoint)); 149 | } 150 | 151 | if (columns > style.max_line_length) { 152 | warnf_style(WC_LONG_LINE, "%s:%zu: line is longer than %zu columns", 153 | line->path, line->lineno, columns); 154 | if (line->len + 1 <= 2048) 155 | print_long_line_tip(WC_LONG_LINE); 156 | } 157 | } 158 | 159 | 160 | int 161 | is_line_blank(struct line *line) 162 | { 163 | char *s = line->data; 164 | while (isspace(*s)) 165 | s++; 166 | return !*s; 167 | } 168 | -------------------------------------------------------------------------------- /ui.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #include "common.h" 3 | 4 | 5 | struct warning_class_data warning_classes[] = { 6 | #define X(ENUM, NAME, ACTION) {NAME, ACTION}, 7 | LIST_WARNING_CLASSES(X) 8 | #undef X 9 | {NULL, 0} 10 | }; 11 | 12 | 13 | static void 14 | vxprintwarningf(enum warning_class class, int severity, const char *fmt, va_list ap) 15 | { 16 | if (warning_classes[class].action != IGNORE) { 17 | fprintf(stderr, "[%s] ", 18 | warning_classes[class].action == INFORM ? "info" : 19 | warning_classes[class].action == WARN_STYLE ? "style" : "warning"); 20 | vfprintf(stderr, fmt, ap); 21 | fprintf(stderr, " (-w%s)\n", warning_classes[class].name); 22 | if (warning_classes[class].action != INFORM) 23 | exit_status = MAX(exit_status, severity); 24 | } 25 | } 26 | 27 | 28 | void 29 | xprintwarningf(enum warning_class class, int severity, const char *fmt, ...) 30 | { 31 | va_list ap; 32 | va_start(ap, fmt); 33 | vxprintwarningf(class, severity, fmt, ap); 34 | va_end(ap); 35 | } 36 | 37 | 38 | void 39 | printinfof(enum warning_class class, const char *fmt, ...) 40 | { 41 | va_list ap; 42 | if (warning_classes[class].action != IGNORE) { 43 | va_start(ap, fmt); 44 | fprintf(stderr, "[info] "); 45 | vfprintf(stderr, fmt, ap); 46 | fprintf(stderr, "\n"); 47 | va_end(ap); 48 | } 49 | } 50 | 51 | 52 | void 53 | printerrorf(const char *fmt, ...) 54 | { 55 | va_list ap; 56 | va_start(ap, fmt); 57 | fprintf(stderr, "%s: [error] ", argv0); 58 | vfprintf(stderr, fmt, ap); 59 | fprintf(stderr, "\n"); 60 | va_end(ap); 61 | exit(EXIT_CRITICAL); 62 | } 63 | 64 | 65 | void 66 | printtipf(enum warning_class class, const char *fmt, ...) 67 | { 68 | va_list ap; 69 | if (warning_classes[class].action != IGNORE) { 70 | va_start(ap, fmt); 71 | fprintf(stderr, "[tip] "); 72 | vfprintf(stderr, fmt, ap); 73 | fprintf(stderr, "\n"); 74 | va_end(ap); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #include "common.h" 3 | 4 | 5 | void * 6 | erealloc(void *ptr, size_t n) 7 | { 8 | void *ret = realloc(ptr, n); 9 | if (!ret) 10 | eprintf("realloc %zu:", n); 11 | return ret; 12 | } 13 | 14 | 15 | void * 16 | ecalloc(size_t n, size_t m) 17 | { 18 | void *ret = calloc(n, m); 19 | if (!ret) 20 | eprintf("calloc %zu %zu:", n, m); 21 | return ret; 22 | } 23 | 24 | 25 | void * 26 | emalloc(size_t n) 27 | { 28 | void *ret = malloc(n); 29 | if (!ret) 30 | eprintf("malloc %zu:", n); 31 | return ret; 32 | } 33 | 34 | 35 | void * 36 | ememdup(const void *ptr, size_t n) 37 | { 38 | void *ret = emalloc(n); 39 | memcpy(ret, ptr, n); 40 | return ret; 41 | } 42 | 43 | 44 | void 45 | eprintf(const char *fmt, ...) 46 | { 47 | va_list ap; 48 | int err = errno; 49 | char end = *fmt ? strchr(fmt, '\0')[-1] : '\0'; 50 | va_start(ap, fmt); 51 | fprintf(stderr, "%s: ", argv0); 52 | vfprintf(stderr, fmt, ap); 53 | if (end == '\0') 54 | fprintf(stderr, "%s\n", strerror(err)); 55 | else if (end == ':') 56 | fprintf(stderr, " %s\n", strerror(err)); 57 | else if (end != '\n') 58 | fprintf(stderr, "\n"); 59 | va_end(ap); 60 | exit(EXIT_ERROR); 61 | } 62 | --------------------------------------------------------------------------------