3 # Copyright (c) 2014, Juniper Networks, Inc.
5 # This SOFTWARE is licensed under the LICENSE provided in the
6 # ../Copyright file. By downloading, installing, copying, or otherwise
7 # using the SOFTWARE, you agree to be bound by the terms of that
9 # Phil Shafer, August 2014
12 # xolint -- a lint for inspecting xo_emit format strings
14 # Yes, that's a long way to go for a pun.
19 while ($ARGV[0] =~ /^-/) {
21 $opt_cpp = 1 if /^-c/;
22 $opt_cflags .= shift @ARGV if /^-C/;
23 $opt_debug = 1 if /^-d/;
24 extract_docs() if /^-D/;
25 $opt_info = $opt_vocabulary = 1 if /^-I/;
26 $opt_print = 1 if /^-p/;
27 $opt_vocabulary = 1 if /^-V/;
28 extract_samples() if /^-X/;
32 print STDERR "xolint [options] files ...\n";
33 print STDERR " -c invoke 'cpp' on input\n";
34 print STDERR " -C flags Pass flags to cpp\n";
35 print STDERR " -d Show debug output\n";
36 print STDERR " -D Extract xolint documentation\n";
37 print STDERR " -I Print xo_info_t data\n";
38 print STDERR " -p Print input data on errors\n";
39 print STDERR " -V Print vocabulary (list of tags)\n";
40 print STDERR " -X Print examples of invalid use\n";
49 print "static xo_info_t xo_info_table[] = {\n";
50 for $name (sort(keys(%vocabulary))) {
51 print " { \"", $name, "\", \"type\", \"desc\" },\n";
54 print "static int xo_info_count = "
55 . "(sizeof(xo_info_table) / sizeof(xo_info_table[0]));\n\n";
56 print "#define XO_SET_INFO() \\\n";
57 print " xo_set_info(NULL, xo_info_table, xo_info_count)\n";
58 } elsif ($opt_vocabulary) {
59 for $name (sort(keys(%vocabulary))) {
67 my $cmd = "grep -B1 -i '$x Should be' $0 | grep xo_emit | sed 's/.*\#*\@//'";
74 my $cmd = "grep -B1 '$x' $0";
76 local @input = <INPUT>;
78 my $ln, $new = 0, $first = 1, $need_nl;
80 for ($ln = 0; $ln <= $#input; $ln++) {
81 chomp($_ = $input[$ln]);
101 $under = "+" x (length($_) + 2);
103 print "'$_'\n$under\n\n";
104 print "The message \"$_\" can be caused by code like:\n";
107 } elsif (/xo_emit\s*\(/) {
109 print "\n::\n\n $_\n\n";
111 } elsif (/^Should be/i) {
112 print "This code should be replaced with code like:\n";
125 local($errors, $warnings, $info) = (0, 0, 0);
126 local $curfile = $file;
130 die "no such file" unless -f $file;
131 open INPUT, "cpp $opt_cflags $file |";
133 open INPUT, $file || die "cannot open input file '$file'";
135 local @input = <INPUT>;
138 local $ln, $rln, $line, $replay;
140 for ($ln = 0; $ln < $#input; $ln++) {
144 if ($line =~ /^\#/) {
145 my($num, $fn) = ($line =~ /\#\s*(\d+)\s+"(.+)"/);
146 ($curfile, $curln) = ($fn, $num) if $num;
150 next unless $line =~ /xo_emit\(/;
152 @tokens = parse_tokens();
153 print "token:\n '" . join("'\n '", @tokens) . "'\n"
155 check_format($tokens[0]);
158 print $file . ": $errors errors, $warnings warnings, $info info\n"
159 unless $opt_vocabulary;
165 my %pairs = ( "{" => "}", "[" => "]", "(" => ")" );
166 my %quotes = ( "\"" => "\"", "'" => "'" );
167 local @data = split(//, $full);
174 $replay = $curln . " " . $line;
178 get_tokens() if $off > $#data;
179 die "out of data" if $off > $#data;
182 print "'$ch' ($quote) ($#open) [" . join("", @open) . "]\n"
185 last if $ch eq ";" && $#open < 0;
187 if ($ch eq "," && $quote eq "" && $#open < 0) {
188 print "[$current]\n" if $opt_debug;
189 push @tokens, $current;
194 next if $ch =~ /[ \t\n\r]/ && $quote eq "" && $#open < 0;
205 $quote = $quotes{$ch};
206 $current = substr($current, 0, -2) if $current =~ /""$/;
211 push @open, $pairs{$ch};
215 if ($#open >= 0 && $ch eq $open[$#open]) {
221 push @tokens, substr($current, 0, -1);
226 if ($ln + 1 < $#input) {
227 $line = $input[++$ln];
229 $replay .= $curln . " " . $line;
230 @data = split(//, $line);
238 return unless $format =~ /^".*"$/;
240 my @data = split(//, $format);
248 local $last, $prev = "";
255 last if $off > $#data;
260 $off += 1 if $ch eq "\\"; # double backslash: "\\/"
271 } elsif ($phase == 0 && $ch eq ":") {
274 } elsif ($ch eq "/") {
281 check_text($build[0]) if length($build[0]);
290 $build[$phase] .= $ch;
294 error("missing closing brace");
297 check_text($build[0]) if length($build[0]);
304 print "checking text: [$text]\n" if $opt_debug;
306 #@ A percent sign appearing in text is a literal
307 #@ xo_emit("cost: %d", cost);
309 #@ xo_emit("{L:cost}: {:cost/%d}", cost);
310 #@ This can be a bit surprising and could be a field that was not
311 #@ properly converted to a libxo-style format string.
312 info("a percent sign appearing in text is a literal") if $text =~ /%/;
327 "start-anchor" => "[",
328 "stop-anchor" => "]",
348 print "checking field: [" . join("][", @field) . "]\n" if $opt_debug;
350 if ($field[0] =~ /,/) {
351 # We have long names; deal with it by turning them into short names
352 my @parts = split(/,/, $field[0]);
354 for (my $i = 1; $i <= $#parts; $i++) {
358 if ($short{$v} eq "@") {
359 # ignore; has no short version
360 } elsif ($short{$v}) {
363 #@ Unknown long name for role/modifier
364 #@ xo_emit("{,humanization:value}", value);
366 #@ xo_emit("{,humanize:value}", value);
367 #@ The hn-* modifiers (hn-decimal, hn-space, hn-1000)
368 #@ are only valid for fields with the {h:} modifier.
369 error("Unknown long name for role/modifier ($v)");
373 $field[4] = substr($field[0], index($field[0], ","));
374 $field[0] = $parts[0] . $new;
377 if ($opt_vocabulary) {
378 $vocabulary{$field[1]} = 1
379 if $field[1] && $field[0] !~ /[DELNPTUW\[\]]/;
383 #@ Last character before field definition is a field type
385 #@ xo_emit("{T:Min} T{:Max}");
387 #@ xo_emit("{T:Min} {T:Max}");
388 #@ Twiddling the "{" and the field role is a common typo.
389 info("last character before field definition is a field type ($last)")
390 if $last =~ /[DELNPTUVW\[\]]/ && $field[0] !~ /[DELNPTUVW\[\]]/;
392 #@ Encoding format uses different number of arguments
393 #@ xo_emit("{:name/%6.6s %%04d/%s}", name, number);
395 #@ xo_emit("{:name/%6.6s %04d/%s-%d}", name, number);
396 #@ Both format should consume the same number of arguments off the stack
397 my $cf = count_args($field[2]);
398 my $ce = count_args($field[3]);
399 warn("encoding format uses different number of arguments ($cf/$ce)")
400 if $ce >= 0 && $cf >= 0 && $ce != $cf;
402 #@ Only one field role can be used
403 #@ xo_emit("{LT:Max}");
405 #@ xo_emit("{T:Max}");
406 my(@roles) = ($field[0] !~ /([DELNPTUVW\[\]]).*([DELNPTUVW\[\]])/);
407 error("only one field role can be used (" . join(", ", @roles) . ")")
410 # Field is a color, note, label, or title
411 if ($field[0] =~ /[CDLNT]/) {
413 #@ Potential missing slash after C, D, N, L, or T with format
414 #@ xo_emit("{T:%6.6s}\n", "Max");
416 #@ xo_emit("{T:/%6.6s}\n", "Max");
417 #@ The "%6.6s" will be a literal, not a field format. While
418 #@ it's possibly valid, it's likely a missing "/".
419 info("potential missing slash after C, D, N, L, or T with format")
422 #@ An encoding format cannot be given (roles: DNLT)
423 #@ xo_emit("{T:Max//%s}", "Max");
424 #@ Fields with the C, D, N, L, and T roles are not emitted in
425 #@ the 'encoding' style (JSON, XML), so an encoding format
426 #@ would make no sense.
427 error("encoding format cannot be given when content is present")
431 # Field is a color, decoration, label, or title
432 if ($field[0] =~ /[CDLN]/) {
433 #@ Format cannot be given when content is present (roles: CDLN)
434 #@ xo_emit("{N:Max/%6.6s}", "Max");
435 #@ Fields with the C, D, L, or N roles can't have both
436 #@ static literal content ("{L:Label}") and a
437 #@ format ("{L:/%s}").
438 #@ This error will also occur when the content has a backslash
439 #@ in it, like "{N:Type of I/O}"; backslashes should be escaped,
440 #@ like "{N:Type of I\\/O}". Note the double backslash, one for
441 #@ handling 'C' strings, and one for libxo.
442 error("format cannot be given when content is present")
443 if $field[1] && $field[2];
446 # Field is a color/effect
447 if ($field[0] =~ /C/) {
450 my @sub = split(/,/, $field[1]);
451 grep { s/^\s*//; s/\s*$//; } @sub;
454 if ($val =~ /^(default,black,red,green,yellow,blue,magenta,cyan,white)$/) {
456 #@ Field has color without fg- or bg- (role: C)
457 #@ xo_emit("{C:green}{:foo}{C:}", x);
459 #@ xo_emit("{C:fg-green}{:foo}{C:}", x);
460 #@ Colors must be prefixed by either "fg-" or "bg-".
461 error("Field has color without fg- or bg- (role: C)");
463 } elsif ($val =~ /^(fg|bg)-(default|black|red|green|yellow|blue|magenta|cyan|white)$/) {
465 } elsif ($val =~ /^(bold|underline)$/) {
466 } elsif ($val =~ /^(no-)?(bold|underline|inverse)$/) {
469 } elsif ($val =~ /^(reset|normal)$/) {
472 #@ Field has invalid color or effect (role: C)
473 #@ xo_emit("{C:fg-purple,bold}{:foo}{C:gween}", x);
475 #@ xo_emit("{C:fg-red,bold}{:foo}{C:fg-green}", x);
476 #@ The list of colors and effects are limited. The
477 #@ set of colors includes default, black, red, green,
478 #@ yellow, blue, magenta, cyan, and white, which must
479 #@ be prefixed by either "fg-" or "bg-". Effects are
480 #@ limited to bold, no-bold, underline, no-underline,
481 #@ inverse, no-inverse, normal, and reset. Values must
482 #@ be separated by commas.
483 error("Field has invalid color or effect (role: C) ($val)");
490 if ($field[0] =~ /h/) {
491 if (length($field[2]) == 0) {
492 #@ Field has humanize modifier but no format string
493 #@ xo_emit("{h:value}", value);
495 #@ xo_emit("{h:value/%d}", value);
496 #@ Humanization is only value for numbers, which are not
497 #@ likely to use the default format ("%s").
498 error("Field has humanize modifier but no format string");
502 # hn-* on non-humanize field
503 if ($field[0] !~ /h/) {
504 if ($field[4] =~ /,hn-/) {
505 #@ Field has hn-* modifier but not 'h' modifier
506 #@ xo_emit("{,hn-1000:value}", value);
508 #@ xo_emit("{h,hn-1000:value}", value);
509 #@ The hn-* modifiers (hn-decimal, hn-space, hn-1000)
510 #@ are only valid for fields with the {h:} modifier.
511 error("Field has hn-* modifier but not 'h' modifier");
516 if (length($field[0]) == 0 || $field[0] =~ /V/) {
518 #@ Value field must have a name (as content)")
519 #@ xo_emit("{:/%s}", "value");
521 #@ xo_emit("{:tag-name/%s}", "value");
522 #@ The field name is used for XML and JSON encodings. These
523 #@ tags names are static and must appear directly in the
525 error("value field must have a name (as content)")
528 #@ Use hyphens, not underscores, for value field name
529 #@ xo_emit("{:no_under_scores}", "bad");
531 #@ xo_emit("{:no-under-scores}", "bad");
532 #@ Use of hyphens is traditional in XML, and the XOF_UNDERSCORES
533 #@ flag can be used to generate underscores in JSON, if desired.
534 #@ But the raw field name should use hyphens.
535 error("use hyphens, not underscores, for value field name")
538 #@ Value field name cannot start with digit
539 #@ xo_emit("{:10-gig/}");
541 #@ xo_emit("{:ten-gig/}");
542 #@ XML element names cannot start with a digit.
543 error("value field name cannot start with digit")
544 if $field[1] =~ /^[0-9]/;
546 #@ Value field name should be lower case
547 #@ xo_emit("{:WHY-ARE-YOU-SHOUTING}", "NO REASON");
549 #@ xo_emit("{:why-are-you-shouting}", "no reason");
550 #@ Lower case is more civilized. Even TLAs should be lower case
551 #@ to avoid scenarios where the differences between "XPath" and
552 #@ "Xpath" drive your users crazy. Lower case rules the seas.
553 error("value field name should be lower case")
554 if $field[1] =~ /[A-Z]/;
556 #@ Value field name should be longer than two characters
557 #@ xo_emit("{:x}", "mumble");
559 #@ xo_emit("{:something-meaningful}", "mumble");
560 #@ Field names should be descriptive, and it's hard to
561 #@ be descriptive in less than two characters. Consider
562 #@ your users and try to make something more useful.
563 #@ Note that this error often occurs when the field type
564 #@ is placed after the colon ("{:T/%20s}"), instead of before
566 error("value field name should be longer than two characters")
567 if $field[1] =~ /[A-Z]/;
569 #@ Value field name contains invalid character
570 #@ xo_emit("{:cost-in-$$/%u}", 15);
572 #@ xo_emit("{:cost-in-dollars/%u}", 15);
573 #@ An invalid character is often a sign of a typo, like "{:]}"
574 #@ instead of "{]:}". Field names are restricted to lower-case
575 #@ characters, digits, and hyphens.
576 error("value field name contains invalid character (" . $field[1] . ")")
577 unless $field[1] =~ /^[0-9a-z-]*$/;
581 if ($field[0] =~ /D/) {
583 #@decoration field contains invalid character
584 #@ xo_emit("{D:not good}");
586 #@ xo_emit("{D:((}{:good}{D:))}", "yes");
587 #@ This is minor, but fields should use proper roles. Decoration
588 #@ fields are meant to hold punctuation and other characters used
589 #@ to decorate the content, typically to make it more readable
591 warn("decoration field contains invalid character")
592 unless $field[1] =~ m:^[~!\@\#\$%^&\*\(\);\:\[\]\{\} ]+$:;
595 if ($field[0] =~ /[\[\]]/) {
596 #@ Anchor content should be decimal width
597 #@ xo_emit("{[:mumble}");
599 #@ xo_emit("{[:32}");
600 #@ Anchors need an integer value to specify the width of
601 #@ the set of anchored fields. The value can be positive
602 #@ (for left padding/right justification) or negative (for
603 #@ right padding/left justification) and can appear in
604 #@ either the start or stop anchor field descriptor.
605 error("anchor content should be decimal width")
606 if $field[1] && $field[1] !~ /^-?\d+$/ ;
608 #@ Anchor format should be "%d"
609 #@ xo_emit("{[:/%s}");
611 #@ xo_emit("{[:/%d}");
612 #@ Anchors only grok integer values, and if the value is not static,
613 #@ if must be in an 'int' argument, represented by the "%d" format.
614 #@ Anything else is an error.
615 error("anchor format should be \"%d\"")
616 if $field[2] && $field[2] ne "%d";
618 #@ Anchor cannot have both format and encoding format")
619 #@ xo_emit("{[:32/%d}");
621 #@ xo_emit("{[:32}");
622 #@ Anchors can have a static value or argument for the width,
623 #@ but cannot have both.
624 error("anchor cannot have both format and encoding format")
625 if $field[1] && $field[2];
632 return -1 unless $format;
635 my($text, $ff, $fc, $rest);
636 for ($in = $format; $in; $in = $rest) {
637 ($text, $ff, $fc, $rest) =
638 ($in =~ /^([^%]*)(%[^%diouxXDOUeEfFgGaAcCsSp]*)([diouxXDOUeEfFgGaAcCsSp])(.*)$/);
641 ($text, $ff, $rest) = ($in =~ /^([^%]*)(%%)(.*)$/);
645 # Not sure what's going on here, but something's wrong...
646 error("invalid field format") if $in =~ /%/;
652 check_field_format($ff, $fc);
658 sub check_field_format {
661 print "check_field_format: [$ff] [$fc]\n" if $opt_debug;
663 my(@chunks) = split(/\./, $ff);
665 #@ Max width only valid for strings
666 #@ xo_emit("{:tag/%2.4.6d}", 55);
668 #@ xo_emit("{:tag/%2.6d}", 55);
669 #@ libxo allows a true 'max width' in addition to the traditional
670 #@ printf-style 'max number of bytes to use for input'. But this
671 #@ is supported only for string values, since it makes no sense
672 #@ for non-strings. This error may occur from a typo,
673 #@ like "{:tag/%6..6d}" where only one period should be used.
674 error("max width only valid for strings")
675 if $#chunks >= 2 && $fc !~ /[sS]/;
679 return if $opt_vocabulary;
680 print STDERR $curfile . ": " .$curln . ": error: " . join(" ", @_) . "\n";
681 print STDERR $replay . "\n" if $opt_print;
686 return if $opt_vocabulary;
687 print STDERR $curfile . ": " .$curln . ": warning: " . join(" ", @_) . "\n";
688 print STDERR $replay . "\n" if $opt_print;
693 return if $opt_vocabulary;
694 print STDERR $curfile . ": " .$curln . ": info: " . join(" ", @_) . "\n";
695 print STDERR $replay . "\n" if $opt_print;