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