/* See LICENSE file for copyright and license details. */ #include "common.h" const char *argv0 = "makel"; static void usage(void) { fprintf(stderr, "%s [-f makefile]\n", argv0); exit(EXIT_ERROR); } int exit_status = 0; struct style style = { .max_line_length = 120, .only_empty_blank_lines = 1, .macro_bracket_style = ROUND }; static void set_line_continuation_joiner(struct line *line) { if (line->len && line->data[line->len - 1] == '\\') { line->data[--line->len] = '\0'; /* Doesn't matter here if the first non-white space is # */ line->continuation_joiner = line->data[0] == '\t' ? '\\' : ' '; } else { line->continuation_joiner = '\0'; } } static void check_line_continuations(struct line *lines, size_t nlines) { size_t i, cont_from = 0; for (i = 0; i < nlines; i++) { set_line_continuation_joiner(&lines[i]); if (lines[i].continuation_joiner && (!i || !lines[i - 1].continuation_joiner) && is_line_blank(&lines[i])) { /* test cases: cont_of_blank.mk */ warnf_confusing(WC_CONTINUATION_OF_BLANK, "%s:%zu: initial line continuation on otherwise blank line, can cause confusion", lines[i].path, lines[i].lineno); } if (!lines[i].continuation_joiner && i && lines[i - 1].continuation_joiner && is_line_blank(&lines[i])) { /* test cases: cont_to_blank.mk */ warnf_confusing(WC_CONTINUATION_TO_BLANK, "%s:%zu: terminal line continuation to blank line, can cause confusion", lines[i].path, lines[i].lineno); } if (lines[i].continuation_joiner && lines[i].eof) { /* test cases: eof_cont.mk (TODO with lines[i].nest_level) */ warnf_unspecified(WC_EOF_LINE_CONTINUATION, "%s:%zu: line continuation at end of file, causes unspecified behaviour%s", lines[i].path, lines[i].lineno, !lines[i].nest_level ? "" : ", it is especially problematic in an included line"); printinfof(WC_EOF_LINE_CONTINUATION, "this implementation will remove the line continuation"); lines[i].continuation_joiner = 0; } if (i && lines[i - 1].continuation_joiner && lines[i].len) { if (!isspace(lines[i].data[0])) { if (lines[cont_from].len && !isspace(lines[cont_from].data[lines[cont_from].len - 1])) { /* test cases: cont_without_ws.mk (TODO with i != cont_from + 1) */ warnf_confusing(WC_SPACELESS_CONTINUATION, "%s:%zu,%zu: is proceeded by a non-white space " "character at the same time as the next line%s begins with " "a non-white space character, this can cause confusion as " "the make utility will add a whitespace", lines[cont_from].path, lines[cont_from].lineno, lines[i].lineno, i == cont_from + 1 ? "" : ", that consist of not only a ,"); } /* test cases: unindented_cont.mk, cont_without_ws.mk */ warnf_confusing(WC_UNINDENTED_CONTINUATION, "%s:%zu: continuation of line is not indented, can cause confusion", lines[i].path, lines[i].lineno); } cont_from = i; } else if (lines[i].continuation_joiner) { cont_from = i; } } } static enum line_class classify_line(struct line *line) { int warned_bad_space = 0; char *s; if (!line->len) return EMPTY; start_over: s = line->data; while (isspace(*s)) { if (!warned_bad_space && !isblank(*s)) { warned_bad_space = 1; /* test cases: bad_ws.mk, noninitial_bad_ws.mk */ warnf_undefined(WC_LEADING_BAD_SPACE, "%s:%zu: line contains leading white space other than " " and , which causes undefined behaviour", line->path, line->lineno); /* TODO what do we do here? */ } s++; } if (*s == '#') { if (line->data[0] != '#') { /* TODO should not apply if command line */ /* test cases: ws_before_comment.mk */ warnf_undefined(WC_ILLEGAL_INDENT, "%s:%zu: comment has leading white space, which is not legal", line->path, line->lineno); printinfof(WC_ILLEGAL_INDENT, "this implementation will recognise it as a comment line"); } return COMMENT; } else if (!*s) { if (line->continuation_joiner) { line++; goto start_over; } return BLANK; } else if (line->data[0] == '\t') { return COMMAND_LINE; } else { if (*s == '-') { /* We will warn about this later */ s++; while (isspace(*s)) s++; } /* TODO unspecified behaviour if include line with */ /* TODO unspecified behaviour if continuation that looks like an include line */ return OTHER; } } int main(int argc, char *argv[]) { const char *path = NULL; struct line *lines; size_t nlines; size_t i; /* make(1) shall support mixing of options and operands (up to --) */ ARGBEGIN { case 'f': cmdline_opt_f(ARG(), &path); break; default: usage(); } ARGEND; if (argc) usage(); setlocale(LC_ALL, ""); /* Required by wcwidth(3) */ lines = load_makefile(path, &nlines); for (i = 0; i < nlines; i++) { check_utf8_encoding(&lines[i]); check_column_count(&lines[i]); } check_line_continuations(lines, nlines); for (i = 0; i < nlines; i++) { switch (classify_line(&lines[i])) { case EMPTY: break; case BLANK: if (style.only_empty_blank_lines) { warnf_style(WC_NONEMPTY_BLANK, "%s:%zu: line is blank but not empty", /* TODO test cases */ lines[i].path, lines[i].lineno); } break; case COMMENT: break; case COMMAND_LINE: /* TODO list may, for historical reasons, end at a comment line; * note, the specifications specify “comment line” which is * define to include empty and blank lines; note however * that a line that begins with a that is prefixed * by whitespace is not a comment line, so, if it begins * with followed by zero or more whitespace, and then * a , it a command line, not a comment line. */ /* TODO on line continuation, remove first '\t', if any, and join with '\\\n' */ case OTHER: /* TODO first non-comment line shall be special target .POSIX without * prerequisites or commands, behaviour is unspecified otherwise */ /* TODO on line continuation, remove leading white space and join with ' ' */ break; default: abort(); } while (lines[i].continuation_joiner) { if (memchr(lines[i].data, '#', lines[i].len)) { /* TODO could also be a non-standard internal macro */ /* test cases: comment_cont.mk */ warnf_confusing(WC_COMMENT_CONTINUATION, "%s:%zu: using continuation of line to continue " "a comment on the next line can cause confusion", lines[i].path, lines[i].lineno); } i += 1; } /* TODO # in comment lines are very problematic. In make(1) * a comment can have a line continuation, but in sh(1) * comments cannot have line continuation. Furthermore, * any #, even if there is a be backslashed or in quotes, * becomes a comment; because of this, some implementations * of make do not recognise comments in command lines and * instead rely on sh(1) ignoring comments (this however * breaks POSIX compliance). */ /* TODO check if a # appears inside quotes or after a backslash */ } free(lines); return exit_status; }