summaryrefslogtreecommitdiffstats
path: root/makel.c
blob: 4a281baf3c4732aa51a7b84889665cfca40085f9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
/* 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: <backslash> 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 <backslash>,");
				}
				/* 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 "
		        	        "<space> and <tab>, 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 <backslash> */
		/* 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 <hash> that is prefixed
			 *      by whitespace is not a comment line, so, if it begins
			 *      with <tab> followed by zero or more whitespace, and then
			 *      a <hash>, 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;
}