#!/usr/bin/env perl1#2# Copyright (c) 2014, Juniper Networks, Inc.3# All rights reserved.4# This SOFTWARE is licensed under the LICENSE provided in the5# ../Copyright file. By downloading, installing, copying, or otherwise6# using the SOFTWARE, you agree to be bound by the terms of that7# LICENSE.8# Phil Shafer, August 20149#10#11# xolint -- a lint for inspecting xo_emit format strings12#13# Yes, that's a long way to go for a pun.1415%vocabulary = ();1617sub main {18while ($ARGV[0] =~ /^-/) {19$_ = shift @ARGV;20$opt_cpp = 1 if /^-c/;21$opt_cflags .= shift @ARGV if /^-C/;22$opt_debug = 1 if /^-d/;23extract_docs() if /^-D/;24$opt_info = $opt_vocabulary = 1 if /^-I/;25$opt_print = 1 if /^-p/;26$opt_vocabulary = 1 if /^-V/;27extract_samples() if /^-X/;28}2930if ($#ARGV < 0) {31print STDERR "xolint [options] files ...\n";32print STDERR " -c invoke 'cpp' on input\n";33print STDERR " -C flags Pass flags to cpp\n";34print STDERR " -d Show debug output\n";35print STDERR " -D Extract xolint documentation\n";36print STDERR " -I Print xo_info_t data\n";37print STDERR " -p Print input data on errors\n";38print STDERR " -V Print vocabulary (list of tags)\n";39print STDERR " -X Print examples of invalid use\n";40exit(1);41}4243for $file (@ARGV) {44parse_file($file);45}4647if ($opt_info) {48print "static xo_info_t xo_info_table[] = {\n";49for $name (sort(keys(%vocabulary))) {50print " { \"", $name, "\", \"type\", \"desc\" },\n";51}52print "};\n";53print "static int xo_info_count = "54. "(sizeof(xo_info_table) / sizeof(xo_info_table[0]));\n\n";55print "#define XO_SET_INFO() \\\n";56print " xo_set_info(NULL, xo_info_table, xo_info_count)\n";57} elsif ($opt_vocabulary) {58for $name (sort(keys(%vocabulary))) {59print $name, "\n";60}61}62}6364sub extract_samples {65my $x = "\#" . "\@";66my $cmd = "grep -B1 -i '$x Should be' $0 | grep xo_emit | sed 's/.*\#*\@//'";67system($cmd);68exit(0);69}7071sub extract_docs {72my $x = "\#" . "\@";73my $cmd = "grep -B1 '$x' $0";74open INPUT, "$cmd |";75local @input = <INPUT>;76close INPUT;77my $ln, $new = 0, $first = 1, $need_nl;7879for ($ln = 0; $ln <= $#input; $ln++) {80chomp($_ = $input[$ln]);81if (/^--/) {82$ln += 1;83$new = 1;84next;85}86if ($first) {87$new = 1;88$first = 0;89next;90}9192s/\s*\#\@\s*//;9394if ($new) {95if ($need_nl) {96print "\n\n";97$need_nl = 0;98}99100$under = "+" x (length($_) + 2);101102print "'$_'\n$under\n\n";103print "The message \"$_\" can be caused by code like:\n";104$new = 0;105106} elsif (/xo_emit\s*\(/) {107s/^\s+//;108print "\n::\n\n $_\n\n";109110} elsif (/^Should be/i) {111print "This code should be replaced with code like:\n";112113} else {114print "$_\n";115$need_nl = 1;116}117}118119exit(0);120}121122sub parse_file {123local($file) = @_;124local($errors, $warnings, $info) = (0, 0, 0);125local $curfile = $file;126local $curln = 0;127128if ($opt_cpp) {129die "no such file" unless -f $file;130open INPUT, "cpp $opt_cflags $file |";131} else {132open INPUT, $file || die "cannot open input file '$file'";133}134local @input = <INPUT>;135close INPUT;136137local $ln, $rln, $line, $replay;138139for ($ln = 0; $ln < $#input; $ln++) {140$line = $input[$ln];141$curln += 1;142143if ($line =~ /^\#/) {144my($num, $fn) = ($line =~ /\#\s*(\d+)\s+"(.+)"/);145($curfile, $curln) = ($fn, $num) if $num;146next;147}148149next unless $line =~ /xo_emit\(/;150151@tokens = parse_tokens();152print "token:\n '" . join("'\n '", @tokens) . "'\n"153if $opt_debug;154check_format($tokens[0]);155}156157print $file . ": $errors errors, $warnings warnings, $info info\n"158unless $opt_vocabulary;159}160161sub parse_tokens {162my $full = "$'";163my @tokens = ();164my %pairs = ( "{" => "}", "[" => "]", "(" => ")" );165my %quotes = ( "\"" => "\"", "'" => "'" );166local @data = split(//, $full);167local @open = ();168local $current = "";169my $quote = "";170local $off = 0;171my $ch;172173$replay = $curln . " " . $line;174$rln = $ln + 1;175176for (;;) {177get_tokens() if $off > $#data;178die "out of data" if $off > $#data;179$ch = $data[$off++];180181print "'$ch' ($quote) ($#open) [" . join("", @open) . "]\n"182if $opt_debug;183184last if $ch eq ";" && $#open < 0;185186if ($ch eq "," && $quote eq "" && $#open < 0) {187print "[$current]\n" if $opt_debug;188push @tokens, $current;189$current = "";190next;191}192193next if $ch =~ /[ \t\n\r]/ && $quote eq "" && $#open < 0;194195$current .= $ch;196197if ($quote) {198if ($ch eq $quote) {199$quote = "";200}201next;202}203if ($quotes{$ch}) {204$quote = $quotes{$ch};205$current = substr($current, 0, -2) if $current =~ /""$/;206next;207}208209if ($pairs{$ch}) {210push @open, $pairs{$ch};211next;212}213214if ($#open >= 0 && $ch eq $open[$#open]) {215pop @open;216next;217}218}219220push @tokens, substr($current, 0, -1);221return @tokens;222}223224sub get_tokens {225if ($ln + 1 < $#input) {226$line = $input[++$ln];227$curln += 1;228$replay .= $curln . " " . $line;229@data = split(//, $line);230$off = 0;231}232}233234sub check_format {235my($format) = @_;236237return unless $format =~ /^".*"$/;238239my @data = split(//, $format);240my $ch;241my $braces = 0;242local $count = 0;243my $content = "";244my $off;245my $phase = 0;246my @build = ();247local $last, $prev = "";248249# Nukes quotes250pop @data;251shift @data;252253for (;;) {254last if $off > $#data;255$ch = $data[$off++];256257if ($ch eq "\\") {258$ch = $data[$off++];259$off += 1 if $ch eq "\\"; # double backslash: "\\/"260next;261}262263if ($braces) {264if ($ch eq "}") {265check_field(@build);266$braces = 0;267@build = ();268$phase = 0;269next;270} elsif ($phase == 0 && $ch eq ":") {271$phase += 1;272next;273} elsif ($ch eq "/") {274$phase += 1;275next;276}277278} else {279if ($ch eq "{") {280check_text($build[0]) if length($build[0]);281$braces = 1;282@build = ();283$last = $prev;284next;285}286$prev = $ch;287}288289$build[$phase] .= $ch;290}291292if ($braces) {293error("missing closing brace");294check_field(@build);295} else {296check_text($build[0]) if length($build[0]);297}298}299300sub check_text {301my($text) = @_;302303print "checking text: [$text]\n" if $opt_debug;304305#@ A percent sign appearing in text is a literal306#@ xo_emit("cost: %d", cost);307#@ Should be:308#@ xo_emit("{L:cost}: {:cost/%d}", cost);309#@ This can be a bit surprising and could be a field that was not310#@ properly converted to a libxo-style format string.311info("a percent sign appearing in text is a literal") if $text =~ /%/;312}313314%short = (315# Roles316"color" => "C",317"decoration" => "D",318"error" => "E",319"label" => "L",320"note" => "N",321"padding" => "P",322"title" => "T",323"units" => "U",324"value" => "V",325"warning" => "W",326"start-anchor" => "[",327"stop-anchor" => "]",328# Modifiers329"colon" => "c",330"display" => "d",331"encoding" => "e",332"hn" => "h",333"hn-decimal" => "@",334"hn-space" => "@",335"hn-1000" => "@",336"humanize" => "h",337"key" => "k",338"leaf-list" => "l",339"no-quotes" => "n",340"quotes" => "q",341"trim" => "t",342"white" => "w",343);344345sub check_field {346my(@field) = @_;347print "checking field: [" . join("][", @field) . "]\n" if $opt_debug;348349if ($field[0] =~ /,/) {350# We have long names; deal with it by turning them into short names351my @parts = split(/,/, $field[0]);352my $new = "";353for (my $i = 1; $i <= $#parts; $i++) {354my $v = $parts[$i];355$v =~ s/^\s+//;356$v =~ s/\s+$//;357if ($short{$v} eq "@") {358# ignore; has no short version359} elsif ($short{$v}) {360$new .= $short{$v};361} else {362#@ Unknown long name for role/modifier363#@ xo_emit("{,humanization:value}", value);364#@ Should be:365#@ xo_emit("{,humanize:value}", value);366#@ The hn-* modifiers (hn-decimal, hn-space, hn-1000)367#@ are only valid for fields with the {h:} modifier.368error("Unknown long name for role/modifier ($v)");369}370}371372$field[4] = substr($field[0], index($field[0], ","));373$field[0] = $parts[0] . $new;374}375376if ($opt_vocabulary) {377$vocabulary{$field[1]} = 1378if $field[1] && $field[0] !~ /[DELNPTUW\[\]]/;379return;380}381382#@ Last character before field definition is a field type383#@ A common typo:384#@ xo_emit("{T:Min} T{:Max}");385#@ Should be:386#@ xo_emit("{T:Min} {T:Max}");387#@ Twiddling the "{" and the field role is a common typo.388info("last character before field definition is a field type ($last)")389if $last =~ /[DELNPTUVW\[\]]/ && $field[0] !~ /[DELNPTUVW\[\]]/;390391#@ Encoding format uses different number of arguments392#@ xo_emit("{:name/%6.6s %%04d/%s}", name, number);393#@ Should be:394#@ xo_emit("{:name/%6.6s %04d/%s-%d}", name, number);395#@ Both format should consume the same number of arguments off the stack396my $cf = count_args($field[2]);397my $ce = count_args($field[3]);398warn("encoding format uses different number of arguments ($cf/$ce)")399if $ce >= 0 && $cf >= 0 && $ce != $cf;400401#@ Only one field role can be used402#@ xo_emit("{LT:Max}");403#@ Should be:404#@ xo_emit("{T:Max}");405my(@roles) = ($field[0] !~ /([DELNPTUVW\[\]]).*([DELNPTUVW\[\]])/);406error("only one field role can be used (" . join(", ", @roles) . ")")407if $#roles > 0;408409# Field is a color, note, label, or title410if ($field[0] =~ /[CDLNT]/) {411412#@ Potential missing slash after C, D, N, L, or T with format413#@ xo_emit("{T:%6.6s}\n", "Max");414#@ should be:415#@ xo_emit("{T:/%6.6s}\n", "Max");416#@ The "%6.6s" will be a literal, not a field format. While417#@ it's possibly valid, it's likely a missing "/".418info("potential missing slash after C, D, N, L, or T with format")419if $field[1] =~ /%/;420421#@ An encoding format cannot be given (roles: DNLT)422#@ xo_emit("{T:Max//%s}", "Max");423#@ Fields with the C, D, N, L, and T roles are not emitted in424#@ the 'encoding' style (JSON, XML), so an encoding format425#@ would make no sense.426error("encoding format cannot be given when content is present")427if $field[3];428}429430# Field is a color, decoration, label, or title431if ($field[0] =~ /[CDLN]/) {432#@ Format cannot be given when content is present (roles: CDLN)433#@ xo_emit("{N:Max/%6.6s}", "Max");434#@ Fields with the C, D, L, or N roles can't have both435#@ static literal content ("{L:Label}") and a436#@ format ("{L:/%s}").437#@ This error will also occur when the content has a backslash438#@ in it, like "{N:Type of I/O}"; backslashes should be escaped,439#@ like "{N:Type of I\\/O}". Note the double backslash, one for440#@ handling 'C' strings, and one for libxo.441error("format cannot be given when content is present")442if $field[1] && $field[2];443}444445# Field is a color/effect446if ($field[0] =~ /C/) {447if ($field[1]) {448my $val;449my @sub = split(/,/, $field[1]);450grep { s/^\s*//; s/\s*$//; } @sub;451452for $val (@sub) {453if ($val =~ /^(default,black,red,green,yellow,blue,magenta,cyan,white)$/) {454455#@ Field has color without fg- or bg- (role: C)456#@ xo_emit("{C:green}{:foo}{C:}", x);457#@ Should be:458#@ xo_emit("{C:fg-green}{:foo}{C:}", x);459#@ Colors must be prefixed by either "fg-" or "bg-".460error("Field has color without fg- or bg- (role: C)");461462} elsif ($val =~ /^(fg|bg)-(default|black|red|green|yellow|blue|magenta|cyan|white)$/) {463# color464} elsif ($val =~ /^(bold|underline)$/) {465} elsif ($val =~ /^(no-)?(bold|underline|inverse)$/) {466# effect467468} elsif ($val =~ /^(reset|normal)$/) {469# effect also470} else {471#@ Field has invalid color or effect (role: C)472#@ xo_emit("{C:fg-purple,bold}{:foo}{C:gween}", x);473#@ Should be:474#@ xo_emit("{C:fg-red,bold}{:foo}{C:fg-green}", x);475#@ The list of colors and effects are limited. The476#@ set of colors includes default, black, red, green,477#@ yellow, blue, magenta, cyan, and white, which must478#@ be prefixed by either "fg-" or "bg-". Effects are479#@ limited to bold, no-bold, underline, no-underline,480#@ inverse, no-inverse, normal, and reset. Values must481#@ be separated by commas.482error("Field has invalid color or effect (role: C) ($val)");483}484}485}486}487488# Humanized field489if ($field[0] =~ /h/) {490if (length($field[2]) == 0) {491#@ Field has humanize modifier but no format string492#@ xo_emit("{h:value}", value);493#@ Should be:494#@ xo_emit("{h:value/%d}", value);495#@ Humanization is only value for numbers, which are not496#@ likely to use the default format ("%s").497error("Field has humanize modifier but no format string");498}499}500501# hn-* on non-humanize field502if ($field[0] !~ /h/) {503if ($field[4] =~ /,hn-/) {504#@ Field has hn-* modifier but not 'h' modifier505#@ xo_emit("{,hn-1000:value}", value);506#@ Should be:507#@ xo_emit("{h,hn-1000:value}", value);508#@ The hn-* modifiers (hn-decimal, hn-space, hn-1000)509#@ are only valid for fields with the {h:} modifier.510error("Field has hn-* modifier but not 'h' modifier");511}512}513514# A value field515if (length($field[0]) == 0 || $field[0] =~ /V/) {516517#@ Value field must have a name (as content)")518#@ xo_emit("{:/%s}", "value");519#@ Should be:520#@ xo_emit("{:tag-name/%s}", "value");521#@ The field name is used for XML and JSON encodings. These522#@ tags names are static and must appear directly in the523#@ field descriptor.524error("value field must have a name (as content)")525unless $field[1];526527#@ Use hyphens, not underscores, for value field name528#@ xo_emit("{:no_under_scores}", "bad");529#@ Should be:530#@ xo_emit("{:no-under-scores}", "bad");531#@ Use of hyphens is traditional in XML, and the XOF_UNDERSCORES532#@ flag can be used to generate underscores in JSON, if desired.533#@ But the raw field name should use hyphens.534error("use hyphens, not underscores, for value field name")535if $field[1] =~ /_/;536537#@ Value field name cannot start with digit538#@ xo_emit("{:10-gig/}");539#@ Should be:540#@ xo_emit("{:ten-gig/}");541#@ XML element names cannot start with a digit.542error("value field name cannot start with digit")543if $field[1] =~ /^[0-9]/;544545#@ Value field name should be lower case546#@ xo_emit("{:WHY-ARE-YOU-SHOUTING}", "NO REASON");547#@ Should be:548#@ xo_emit("{:why-are-you-shouting}", "no reason");549#@ Lower case is more civilized. Even TLAs should be lower case550#@ to avoid scenarios where the differences between "XPath" and551#@ "Xpath" drive your users crazy. Lower case rules the seas.552error("value field name should be lower case")553if $field[1] =~ /[A-Z]/;554555#@ Value field name should be longer than two characters556#@ xo_emit("{:x}", "mumble");557#@ Should be:558#@ xo_emit("{:something-meaningful}", "mumble");559#@ Field names should be descriptive, and it's hard to560#@ be descriptive in less than two characters. Consider561#@ your users and try to make something more useful.562#@ Note that this error often occurs when the field type563#@ is placed after the colon ("{:T/%20s}"), instead of before564#@ it ("{T:/20s}").565error("value field name should be longer than two characters")566if $field[1] =~ /[A-Z]/;567568#@ Value field name contains invalid character569#@ xo_emit("{:cost-in-$$/%u}", 15);570#@ Should be:571#@ xo_emit("{:cost-in-dollars/%u}", 15);572#@ An invalid character is often a sign of a typo, like "{:]}"573#@ instead of "{]:}". Field names are restricted to lower-case574#@ characters, digits, and hyphens.575error("value field name contains invalid character (" . $field[1] . ")")576unless $field[1] =~ /^[0-9a-z-]*$/;577}578579# A decoration field580if ($field[0] =~ /D/) {581582#@decoration field contains invalid character583#@ xo_emit("{D:not good}");584#@ Should be:585#@ xo_emit("{D:((}{:good}{D:))}", "yes");586#@ This is minor, but fields should use proper roles. Decoration587#@ fields are meant to hold punctuation and other characters used588#@ to decorate the content, typically to make it more readable589#@ to human readers.590warn("decoration field contains invalid character")591unless $field[1] =~ m:^[~!\@\#\$%^&\*\(\);\:\[\]\{\} ]+$:;592}593594if ($field[0] =~ /[\[\]]/) {595#@ Anchor content should be decimal width596#@ xo_emit("{[:mumble}");597#@ Should be:598#@ xo_emit("{[:32}");599#@ Anchors need an integer value to specify the width of600#@ the set of anchored fields. The value can be positive601#@ (for left padding/right justification) or negative (for602#@ right padding/left justification) and can appear in603#@ either the start or stop anchor field descriptor.604error("anchor content should be decimal width")605if $field[1] && $field[1] !~ /^-?\d+$/ ;606607#@ Anchor format should be "%d"608#@ xo_emit("{[:/%s}");609#@ Should be:610#@ xo_emit("{[:/%d}");611#@ Anchors only grok integer values, and if the value is not static,612#@ if must be in an 'int' argument, represented by the "%d" format.613#@ Anything else is an error.614error("anchor format should be \"%d\"")615if $field[2] && $field[2] ne "%d";616617#@ Anchor cannot have both format and encoding format")618#@ xo_emit("{[:32/%d}");619#@ Should be:620#@ xo_emit("{[:32}");621#@ Anchors can have a static value or argument for the width,622#@ but cannot have both.623error("anchor cannot have both format and encoding format")624if $field[1] && $field[2];625}626}627628sub count_args {629my($format) = @_;630631return -1 unless $format;632633my $in;634my($text, $ff, $fc, $rest);635for ($in = $format; $in; $in = $rest) {636($text, $ff, $fc, $rest) =637($in =~ /^([^%]*)(%[^%diouxXDOUeEfFgGaAcCsSp]*)([diouxXDOUeEfFgGaAcCsSp])(.*)$/);638unless ($ff) {639# Might be a "%%"640($text, $ff, $rest) = ($in =~ /^([^%]*)(%%)(.*)$/);641if ($ff) {642check_text($text);643} else {644# Not sure what's going on here, but something's wrong...645error("invalid field format") if $in =~ /%/;646}647next;648}649650check_text($text);651check_field_format($ff, $fc);652}653654return 0;655}656657sub check_field_format {658my($ff, $fc) = @_;659660print "check_field_format: [$ff] [$fc]\n" if $opt_debug;661662my(@chunks) = split(/\./, $ff);663664#@ Max width only valid for strings665#@ xo_emit("{:tag/%2.4.6d}", 55);666#@ Should be:667#@ xo_emit("{:tag/%2.6d}", 55);668#@ libxo allows a true 'max width' in addition to the traditional669#@ printf-style 'max number of bytes to use for input'. But this670#@ is supported only for string values, since it makes no sense671#@ for non-strings. This error may occur from a typo,672#@ like "{:tag/%6..6d}" where only one period should be used.673error("max width only valid for strings")674if $#chunks >= 2 && $fc !~ /[sS]/;675}676677sub error {678return if $opt_vocabulary;679print STDERR $curfile . ": " .$curln . ": error: " . join(" ", @_) . "\n";680print STDERR $replay . "\n" if $opt_print;681$errors += 1;682}683684sub warn {685return if $opt_vocabulary;686print STDERR $curfile . ": " .$curln . ": warning: " . join(" ", @_) . "\n";687print STDERR $replay . "\n" if $opt_print;688$warnings += 1;689}690691sub info {692return if $opt_vocabulary;693print STDERR $curfile . ": " .$curln . ": info: " . join(" ", @_) . "\n";694print STDERR $replay . "\n" if $opt_print;695$info += 1;696}697698main: {699main();700}701702703