1 /*
2 ** cdecl -- C gibberish translator
3 ** src/print.c
4 **
5 ** Copyright (C) 2017-2021 Paul J. Lucas
6 **
7 ** This program is free software: you can redistribute it and/or modify
8 ** it under the terms of the GNU General Public License as published by
9 ** the Free Software Foundation, either version 3 of the License, or
10 ** (at your option) any later version.
11 **
12 ** This program is distributed in the hope that it will be useful,
13 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ** GNU General Public License for more details.
16 **
17 ** You should have received a copy of the GNU General Public License
18 ** along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 /**
22 * @file
23 * Defines functions for printing error and warning messages.
24 */
25
26 // local
27 #include "pjl_config.h" /* must go first */
28 #include "print.h"
29 #include "c_keyword.h"
30 #include "c_lang.h"
31 #include "c_sname.h"
32 #include "cdecl.h"
33 #include "cdecl_keyword.h"
34 #include "color.h"
35 #include "lexer.h"
36 #include "options.h"
37 #include "strbuf.h"
38 #include "util.h"
39
40 /// @cond DOXYGEN_IGNORE
41
42 // standard
43 #include <assert.h>
44 #include <ctype.h>
45 #include <stdarg.h>
46 #include <stdlib.h>
47 #include <string.h>
48
49 /// @endcond
50
51 /// @cond DOXYGEN_IGNORE
52
53 // local constants
54 static char const *const MORE[] = { "...", "..." };
55 static size_t const MORE_LEN[] = { 3, 3 };
56 static unsigned const TERM_COLUMNS_DEFAULT = 80;
57
58 /// @endcond
59
60 // local functions
61 static void print_input_line( size_t*, size_t );
62
63 PJL_WARN_UNUSED_RESULT
64 static size_t token_len( char const*, size_t, size_t );
65
66 // extern variables
67 print_params_t print_params;
68
69 ////////// local functions ////////////////////////////////////////////////////
70
71 /**
72 * Helper function for print_suggestions() and fprint_list() that gets the
73 * string for a \ref did_you_mean token.
74 *
75 * @param elt A pointer to a \ref did_you_mean element.
76 * @return Returns a pointer to the next "Did you mean" suggestion string or
77 * NULL if none.
78 */
79 PJL_WARN_UNUSED_RESULT
fprint_list_get_dym(void const * elt)80 static char const* fprint_list_get_dym( void const *elt ) {
81 did_you_mean_t const *const dym = elt;
82 if ( dym->token == NULL )
83 return NULL;
84
85 static strbuf_t sbufs[ 2 ];
86 static unsigned buf_index;
87
88 strbuf_t *const sbuf = &sbufs[ buf_index++ % ARRAY_SIZE( sbufs ) ];
89 strbuf_reset( sbuf );
90 strbuf_printf( sbuf, "\"%s\"", dym->token );
91 return sbuf->str;
92 }
93
94 /**
95 * Gets the current input line.
96 *
97 * @param input_line_len A pointer to receive the length of the input line.
98 * @return Returns the input line.
99 */
100 PJL_WARN_UNUSED_RESULT
get_input_line(size_t * input_line_len)101 static char const* get_input_line( size_t *input_line_len ) {
102 char const *input_line = lexer_input_line( input_line_len );
103 assert( input_line != NULL );
104 if ( *input_line_len == 0 ) { // no input? try command line
105 input_line = print_params.command_line;
106 assert( input_line != NULL );
107 *input_line_len = print_params.command_line_len;
108 }
109 if ( *input_line_len >= print_params.inserted_len ) {
110 input_line += print_params.inserted_len;
111 *input_line_len -= print_params.inserted_len;
112 }
113
114 //
115 // Chop off whitespace (if any) so we can always print a newline ourselves.
116 //
117 str_rtrim_len( input_line, input_line_len );
118
119 return input_line;
120 }
121
122 /**
123 * Prints the error line (if not interactive) and a `^` (in color, if possible
124 * and requested) under the offending token.
125 *
126 * @param error_column The zero-based column of the offending token.
127 * @return Returns \a error_column, adjusted (if necessary).
128 */
129 PJL_WARN_UNUSED_RESULT
print_caret(size_t error_column)130 static size_t print_caret( size_t error_column ) {
131 if ( error_column >= print_params.inserted_len )
132 error_column -= print_params.inserted_len;
133
134 unsigned term_columns;
135 #ifdef ENABLE_TERM_SIZE
136 get_term_columns_lines( &term_columns, /*plines=*/NULL );
137 if ( term_columns == 0 )
138 #endif /* ENABLE_TERM_SIZE */
139 term_columns = TERM_COLUMNS_DEFAULT;
140
141 if ( is_input_a_tty || opt_interactive ) {
142 //
143 // If we're interactive, we can put the ^ under the already existing token
144 // the user typed for the recent command, but we have to add the length of
145 // the prompt.
146 //
147 error_column += strlen( OPT_LANG_IS(C_ANY) ? CDECL : CPPDECL )
148 + 2 /* "> " */;
149 if ( term_columns > 0 )
150 error_column %= term_columns;
151 }
152 else {
153 //
154 // Otherwise we have to print out the line containing the error then put
155 // the ^ under that.
156 //
157 print_input_line( &error_column, term_columns );
158 }
159
160 EPRINTF( "%*s", STATIC_CAST( int, error_column ), "" );
161 SGR_START_COLOR( stderr, caret );
162 EPUTC( '^' );
163 SGR_END_COLOR( stderr );
164 EPUTC( '\n' );
165
166 return error_column;
167 }
168
169 /**
170 * Prints the input line, "scrolled" to the left with `...` printed if
171 * necessary, so that \a error_column is always within \a term_columns.
172 *
173 * @param error_column A pointer to the zero-based column of the offending
174 * token. It is adjusted if necessary to be the terminal column at which the
175 * `^` should be printed.
176 * @param term_columns The number of columns of the terminal.
177 */
print_input_line(size_t * error_column,size_t term_columns)178 static void print_input_line( size_t *error_column, size_t term_columns ) {
179 size_t input_line_len;
180 char const *input_line = get_input_line( &input_line_len );
181 assert( input_line != NULL );
182 assert( input_line_len > 0 );
183
184 if ( *error_column > input_line_len )
185 *error_column = input_line_len;
186
187 --term_columns; // more aesthetically pleasing
188
189 //
190 // If the error is due to unexpected end of input, back up the error
191 // column so it refers to a non-null character.
192 //
193 if ( *error_column > 0 && input_line[ *error_column ] == '\0' )
194 --*error_column;
195
196 size_t const token_columns =
197 token_len( input_line, input_line_len, *error_column );
198 size_t const error_end_column = *error_column + token_columns - 1;
199
200 //
201 // Start with the number of printable columns equal to the length of the
202 // line.
203 //
204 size_t print_columns = input_line_len;
205
206 //
207 // If the number of printable columns exceeds the number of terminal
208 // columns, there is "more" on the right, so limit the number of printable
209 // columns.
210 //
211 bool more[2]; // [0] = left; [1] = right
212 more[1] = print_columns > term_columns;
213 if ( more[1] )
214 print_columns = term_columns;
215
216 //
217 // If the error end column is past the number of printable columns, there
218 // is "more" on the left since we will "scroll" the line to the left.
219 //
220 more[0] = error_end_column > print_columns;
221
222 //
223 // However, if there is "more" on the right but the end of the error token
224 // is at the end of the line, then we can print through the end of the line
225 // without any "more."
226 //
227 if ( more[1] ) {
228 if ( error_end_column < input_line_len - 1 )
229 print_columns -= MORE_LEN[1];
230 else
231 more[1] = false;
232 }
233
234 if ( more[0] ) {
235 //
236 // There is "more" on the left so we have to adjust the error column, the
237 // number of printable columns, and the offset into the input line that we
238 // start printing at to give the appearance that the input line has been
239 // "scrolled" to the left.
240 //
241 assert( print_columns >= token_columns );
242 size_t const error_column_term = print_columns - token_columns;
243 print_columns -= MORE_LEN[0];
244 assert( *error_column > error_column_term );
245 input_line += MORE_LEN[0] + (*error_column - error_column_term);
246 *error_column = error_column_term;
247 }
248
249 EPRINTF( "%s%.*s%s\n",
250 (more[0] ? MORE[0] : ""),
251 STATIC_CAST( int, print_columns ), input_line,
252 (more[1] ? MORE[1] : "")
253 );
254 }
255
256 /**
257 * Gets the length of the first token in \a s. Characters are divided into
258 * three classes:
259 *
260 * + Whitespace.
261 * + Alpha-numeric.
262 * + Everything else (e.g., punctuation).
263 *
264 * A token is composed of characters in exclusively one class. The class is
265 * determined by `s[0]`. The length of the token is the number of consecutive
266 * characters of the same class starting at `s[0]`.
267 *
268 * @param s The string to use.
269 * @param s_len The length of \a s.
270 * @param token_offset The offset of the start of the token.
271 * @return Returns the length of the token.
272 */
273 PJL_WARN_UNUSED_RESULT
token_len(char const * s,size_t s_len,size_t token_offset)274 static size_t token_len( char const *s, size_t s_len, size_t token_offset ) {
275 assert( s != NULL );
276
277 char const *const end = s + s_len;
278 s += token_offset;
279
280 if ( s >= end )
281 return 0;
282
283 bool const is_s0_alnum = isalnum( s[0] );
284 bool const is_s0_space = isspace( s[0] );
285
286 char const *const s0 = s;
287 while ( ++s < end && *s != '\0' ) {
288 if ( is_s0_alnum ) {
289 if ( !isalnum( *s ) )
290 break;
291 }
292 else if ( is_s0_space ) {
293 if ( !isspace( *s ) )
294 break;
295 }
296 else {
297 if ( isalnum( *s ) || isspace( *s ) )
298 break;
299 }
300 } // while
301 return STATIC_CAST( size_t, s - s0 );
302 }
303
304 ////////// extern functions ///////////////////////////////////////////////////
305
fl_print_error(char const * file,int line,c_loc_t const * loc,char const * format,...)306 void fl_print_error( char const *file, int line, c_loc_t const *loc,
307 char const *format, ... ) {
308 assert( file != NULL );
309 assert( format != NULL );
310
311 if ( loc != NULL ) {
312 print_loc( loc );
313 SGR_START_COLOR( stderr, error );
314 EPUTS( "error" );
315 SGR_END_COLOR( stderr );
316 EPUTS( ": " );
317 }
318
319 print_debug_file_line( file, line );
320
321 va_list args;
322 va_start( args, format );
323 vfprintf( stderr, format, args );
324 va_end( args );
325 }
326
fl_print_error_unknown_name(char const * file,int line,c_loc_t const * loc,c_sname_t const * sname)327 void fl_print_error_unknown_name( char const *file, int line,
328 c_loc_t const *loc, c_sname_t const *sname ) {
329 assert( sname != NULL );
330
331 dym_kind_t dym_kind = DYM_NONE;
332
333 // Must dup this since c_sname_full_name() returns a temporary buffer.
334 char const *const name = check_strdup( c_sname_full_name( sname ) );
335
336 c_keyword_t const *const k =
337 c_keyword_find( name, LANG_ANY, C_KW_CTX_DEFAULT );
338 if ( k != NULL ) {
339 char const *what = NULL;
340
341 switch ( c_tid_tpid( k->tid ) ) {
342 case C_TPID_NONE: // e.g., "break"
343 case C_TPID_STORE: // e.g., "extern"
344 dym_kind = DYM_C_KEYWORDS;
345 what = "keyword";
346 break;
347 case C_TPID_BASE: // e.g., "char"
348 dym_kind = DYM_C_TYPES;
349 what = "type";
350 break;
351 case C_TPID_ATTR:
352 dym_kind = DYM_C_ATTRIBUTES; // e.g., "noreturn"
353 what = "attribute";
354 break;
355 } // switch
356
357 fl_print_error( file, line, loc,
358 "\"%s\": unsupported %s%s", name, what, c_lang_which( k->lang_ids )
359 );
360 }
361 else {
362 fl_print_error( file, line, loc, "\"%s\": unknown name", name );
363
364 dym_kind = DYM_C_KEYWORDS | DYM_C_TYPES;
365 if ( cdecl_mode == CDECL_ENGLISH_TO_GIBBERISH ) {
366 dym_kind |= DYM_CDECL_KEYWORDS;
367 cdecl_keyword_t const *const ck = cdecl_keyword_find( name );
368 if ( ck != NULL )
369 EPRINTF( " (\"%s\" is a cdecl keyword)", name );
370 }
371 }
372
373 print_suggestions( dym_kind, name );
374 EPUTC( '\n' );
375 FREE( name );
376 }
377
fl_print_warning(char const * file,int line,c_loc_t const * loc,char const * format,...)378 void fl_print_warning( char const *file, int line, c_loc_t const *loc,
379 char const *format, ... ) {
380 assert( file != NULL );
381 assert( format != NULL );
382
383 if ( loc != NULL )
384 print_loc( loc );
385 SGR_START_COLOR( stderr, warning );
386 EPUTS( "warning" );
387 SGR_END_COLOR( stderr );
388 EPUTS( ": " );
389
390 print_debug_file_line( file, line );
391
392 va_list args;
393 va_start( args, format );
394 vfprintf( stderr, format, args );
395 va_end( args );
396 }
397
print_debug_file_line(char const * file,int line)398 void print_debug_file_line( char const *file, int line ) {
399 assert( file != NULL );
400 #ifdef ENABLE_CDECL_DEBUG
401 if ( opt_cdecl_debug )
402 EPRINTF( "[%s:%d] ", file, line );
403 #else
404 (void)file;
405 (void)line;
406 #endif /* ENABLE_CDECL_DEBUG */
407 }
408
print_hint(char const * format,...)409 void print_hint( char const *format, ... ) {
410 assert( format != NULL );
411 EPUTS( "; did you mean " );
412 va_list args;
413 va_start( args, format );
414 vfprintf( stderr, format, args );
415 va_end( args );
416 EPUTS( "?\n" );
417 }
418
print_loc(c_loc_t const * loc)419 void print_loc( c_loc_t const *loc ) {
420 assert( loc != NULL );
421 size_t const column = print_caret( STATIC_CAST( size_t, loc->first_column ) );
422 SGR_START_COLOR( stderr, locus );
423 if ( opt_conf_file != NULL )
424 EPRINTF( "%s:%d,", opt_conf_file, loc->first_line + 1 );
425 EPRINTF( "%zu", column + 1 );
426 SGR_END_COLOR( stderr );
427 EPUTS( ": " );
428 }
429
print_suggestions(dym_kind_t kinds,char const * unknown_token)430 bool print_suggestions( dym_kind_t kinds, char const *unknown_token ) {
431 did_you_mean_t const *const dym = dym_new( kinds, unknown_token );
432 if ( dym != NULL ) {
433 EPUTS( "; did you mean " );
434 fprint_list( stderr, dym, sizeof *dym, &fprint_list_get_dym );
435 EPUTC( '?' );
436 dym_free( dym );
437 return true;
438 }
439 return false;
440 }
441
442 ///////////////////////////////////////////////////////////////////////////////
443 /* vim:set et sw=2 ts=2: */
444