1 ------------------------------------------------------------------------------
4 -- Copyright (C) 2006-2007, Pascal Obry --
6 -- This library is free software; you can redistribute it and/or modify --
7 -- it under the terms of the GNU General Public License as published by --
8 -- the Free Software Foundation; either version 2 of the License, or (at --
9 -- your option) any later version. --
11 -- This library is distributed in the hope that it will be useful, but --
12 -- WITHOUT ANY WARRANTY; without even the implied warranty of --
13 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU --
14 -- General Public License for more details. --
16 -- You should have received a copy of the GNU General Public License --
17 -- along with this library; if not, write to the Free Software Foundation, --
18 -- Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. --
20 ------------------------------------------------------------------------------
25 -- style_checker [options] [-lang name] [options]
27 -- The first options are set for all available languages.
28 -- Options that are set after a -lang name are only set for this specific
29 -- language (language names are not case sensitive).
31 -- To display the usage information:
34 -- To check Ada files only (syntax, line length, trailing spaces):
35 -- $ style_checker -BCELS -lang Ada -slt file.ad*
37 -- To list available languages:
38 -- $ style_checker -lang
42 with Ada
.Command_Line
;
43 with Ada
.Containers
.Indefinite_Hashed_Sets
;
45 with Ada
.IO_Exceptions
;
46 with Ada
.Strings
.Fixed
;
47 with Ada
.Strings
.Hash
;
48 with Ada
.Strings
.Unbounded
;
51 with GNAT
.Command_Line
;
58 with Supported_Languages
;
60 procedure Style_Checker
is
64 use Ada
.Strings
.Unbounded
;
67 use type Directories
.File_Kind
;
68 use type Checks
.Line_Ending_Style
;
71 package Ext_Set
is new Containers
.Indefinite_Hashed_Sets
72 (String, Hash
, "=", "=");
74 Y
: constant String :=
75 Calendar
.Year_Number
'Image (Calendar
.Year
(Calendar
.Clock
));
76 Current_Year
: constant String := Y
(Y
'First + 1 .. Y
'Last);
78 Absolute_Pathname
: Boolean := False;
79 Style_Error
: Boolean := False;
80 Ignore_Set
: Ext_Set
.Set
;
81 Max_Error
: Natural := Natural'Last;
82 Error_Count
: Natural := 0;
83 Real_Filename
: Unbounded_String
;
85 type File_Checker
is record
86 File
: File_Reader
.File_Type
;
87 Lang
: Languages
.Lang_Access
;
88 Count_Blank
: Natural := 0;
89 Copyright_Found
: Boolean := False;
90 Copyright_Year
: Boolean := False;
91 Header_Size
: Natural := 0;
92 In_Header
: Boolean := True;
93 Consecutive_Comment
: Natural := 0;
94 Last_Comment_Dot_EOL
: Boolean := False;
97 procedure Check
(Filename
: in String);
101 (Checker
: in out File_Checker
;
103 Line_Ending
: in Checks
.Line_Ending_Style
);
104 -- Pass all checks that are line related
106 subtype Line_Offset
is Integer range -1 .. 0;
108 procedure Report_Error
109 (File
: in File_Reader
.File_Type
;
111 Offset
: in Line_Offset
:= 0);
112 -- Report an error to standard error
114 procedure Report_Error
115 (Filename
: in String;
117 At_Line
: in Natural := 1);
118 -- Report an error to standard error
121 -- Display the usage information
123 procedure List_Languages
;
124 -- Display supported languages
126 function Unquote
(Str
: in String) return String;
127 -- Removes leading/trailing spaces and quote if present
133 procedure Check
(Filename
: in String) is
134 Checker
: File_Checker
;
135 Line
: String (1 .. 2_048
);
137 Nb_Line
: Natural := 0;
138 Ending
: Checks
.Line_Ending_Style
;
140 Checker
.Lang
:= new Languages
.Lang
'Class'(Languages.Get (Filename));
142 -- Run line oriented tests
144 File_Reader.Open (Checker.File, Filename);
146 while not File_Reader.End_Of_File (Checker.File) loop
147 File_Reader.Get_Line (Checker.File, Line, K, Ending);
148 Check_Line (Checker, Line (1 .. K), Ending);
151 Nb_Line := File_Reader.Line (Checker.File);
153 File_Reader.Close (Checker.File);
155 -- Run file oriented tests
157 if Checker.Lang.Get_Syntax_Check then
158 if not Languages.Run_Syntax_Check (Checker.Lang.all, Filename) then
163 if Checker.Lang.Get_Header_Size > Checker.Header_Size then
164 if Checker.Header_Size = 0 then
166 (Filename, "missing file header (must start on first line)");
169 (Filename, "file header should have at least"
170 & Positive'Image (Checker.Lang.Get_Header_Size)
175 if Checker.Lang.Get_Copyright_Present
176 and then not Checker.Copyright_Found
178 Report_Error (Filename, "missing copyright notice");
181 if Checker.Copyright_Found
182 and then Checker.Lang.Get_Copyright_Year
183 and then not Checker.Copyright_Year
186 (Filename, "missing year " & Current_Year & " in copyright");
189 if Checker.Lang.Get_Duplicate_Blank_Line = Checks.Rejected
190 and then Checker.Count_Blank >= 1
193 (Filename => Filename,
194 Message => "blank line not allowed at end of file",
199 when IO_Exceptions.Name_Error =>
200 Report_Error (Filename, "can't open file");
208 (Checker : in out File_Checker;
210 Line_Ending : in Checks.Line_Ending_Style)
212 procedure Check_Ending;
214 procedure Check_Length_Max;
216 procedure Check_Duplicate_Blank;
218 procedure Check_Trailing_Spaces;
220 procedure Check_Header;
222 procedure Check_Copyright;
224 procedure Check_Space_Comment;
226 procedure Check_Comment_Dot_EOL;
228 ---------------------------
229 -- Check_Comment_Dot_EOL --
230 ---------------------------
232 procedure Check_Comment_Dot_EOL is
234 if not Checker.Lang.Get_Comment_Dot_EOL
235 and then Checker.Lang.Comment /= ""
237 if Fixed.Index (Line, String'(Checker
.Lang
.Comment
)) /= 0 then
239 Checker
.Consecutive_Comment
:= Checker
.Consecutive_Comment
+ 1;
242 (Fixed
.Index_Non_Blank
(Line
, Going
=> Backward
)) = '.'
244 Checker
.Last_Comment_Dot_EOL
:= True;
246 Checker
.Last_Comment_Dot_EOL
:= False;
250 -- No more in a comment line
252 if Checker
.Consecutive_Comment
= 1
253 and then Checker
.Last_Comment_Dot_EOL
257 "single line comment should not terminate with dot",
261 Checker
.Consecutive_Comment
:= 0;
262 Checker
.Last_Comment_Dot_EOL
:= False;
265 end Check_Comment_Dot_EOL
;
267 ---------------------
268 -- Check_Copyright --
269 ---------------------
271 procedure Check_Copyright
is
273 Co_Start
: Natural := 0;
274 Cp_Start
: Natural := Fixed
.Index
(Line
, " Copyright");
276 if Checker
.Lang
.Comment
/= "" then
277 Co_Start
:= Fixed
.Index
(Line
, String'(Checker.Lang.Comment));
281 and then Cp_Start + 10 <= Line'Length
282 and then Line (Cp_Start + 10) /= ' '
284 -- We are not at the end of the line and no space after Copyright
288 if (Checker.Lang.Get_Copyright_Present
289 or else Checker.Lang.Get_Copyright_Year)
290 and then Cp_Start /= 0
291 and then Co_Start /= 0
292 and then Cp_Start > Co_Start
294 Checker.Copyright_Found := True;
296 if Checker.Lang.Get_Copyright_Year then
297 if Fixed.Index (Line, Current_Year) /= 0 then
298 Checker.Copyright_Year := True;
303 -- Check that the copyright year follow the given regexp
306 and then Checker.Lang.Get_Copyright_Pattern /= ""
309 Pattern : constant Regpat.Pattern_Matcher :=
310 Regpat.Compile (Checker.Lang.Get_Copyright_Pattern);
312 if not Regpat.Match (Pattern, Line) then
315 "copyright line not matching expected pattern");
321 ---------------------------
322 -- Check_Duplicate_Blank --
323 ---------------------------
325 procedure Check_Duplicate_Blank is
327 if Checker.Lang.Get_Duplicate_Blank_Line = Checks.Rejected
328 and then (Line'Length = 0
329 or else Fixed.Count (Line, " " & ASCII.HT) = Line'Length)
331 Checker.Count_Blank := Checker.Count_Blank + 1;
333 if Checker.Count_Blank > 1 then
334 Report_Error (Checker.File, "duplicate blank line");
338 Checker.Count_Blank := 0;
340 end Check_Duplicate_Blank;
346 procedure Check_Ending is
348 if Checker.Lang.Get_Line_Ending /= Checks.Any then
349 if Line_Ending = Checks.No then
352 "missing line terminator");
353 elsif Checker.Lang.Get_Line_Ending /= Line_Ending then
356 "wrong " & Checks.Line_Ending_Style'Image (Line_Ending) &
366 procedure Check_Header is
367 C : constant String := Checker.Lang.Comment;
370 and then Line'Length >= C'Length
371 and then Line (Line'First .. Line'First + C'Length - 1) = C
373 Checker.Header_Size := Checker.Header_Size + 1;
375 Checker.In_Header := False;
379 ----------------------
380 -- Check_Length_Max --
381 ----------------------
383 procedure Check_Length_Max is
385 if Line'Length > Checker.Lang.Get_Line_Length_Max then
386 Report_Error (Checker.File, "line too long");
388 end Check_Length_Max;
390 -------------------------
391 -- Check_Space_Comment --
392 -------------------------
394 procedure Check_Space_Comment is
395 N : constant Natural := Checker.Lang.Get_Space_Comment;
396 NI : constant String := Natural'Image (N);
397 C : constant String := Checker.Lang.Comment;
398 I : constant Natural := Fixed.Index_Non_Blank (Line);
402 and then I + C'Length - 1 <= Line'Last
403 and then Line (I .. I + C'Length - 1) = C
404 and then Line (Line'Last - C'Length + 1 .. Line'Last) /= C
405 and then (Line (I .. I + 1) /= "#!"
406 or else File_Reader.Line (Checker.File) > 1)
407 -- Do no check script headers
409 for K in I + C'Length .. I + C'Length + N - 1 loop
410 if Line (K) /= ' ' then
413 NI (NI'First + 1 .. NI'Last) & " spaces after " & C);
418 end Check_Space_Comment;
420 ---------------------------
421 -- Check_Trailing_Spaces --
422 ---------------------------
424 procedure Check_Trailing_Spaces is
426 if Checker.Lang.Get_Trailing_Spaces = Checks.Rejected
427 and then Line'Length > 0
428 and then (Line (Line'Last) = ' '
429 or else Line (Line'Last) = ASCII.HT)
431 Report_Error (Checker.File, "no trailing spaces allowed");
433 end Check_Trailing_Spaces;
438 Check_Duplicate_Blank;
439 Check_Trailing_Spaces;
443 Check_Comment_Dot_EOL;
450 procedure List_Languages is
451 procedure P (Str : in String) renames Text_IO.Put_Line;
454 P ("Style Checker " & Version.Simple);
464 procedure Report_Error
465 (File : in File_Reader.File_Type;
467 Offset : in Line_Offset := 0)
469 Line : constant String :=
470 Natural'Image (File_Reader.Line (File) + Offset);
472 Error_Count := Error_Count + 1;
473 if Error_Count <= Max_Error then
474 if Real_Filename = Null_Unbounded_String then
476 (Text_IO.Standard_Error,
477 File_Reader.Name (File, Absolute_Pathname) & ':'
478 & Line (Line'First + 1 .. Line'Last) & ": " & Message);
481 (Text_IO.Standard_Error,
482 To_String (Real_Filename) & ':'
483 & Line (Line'First + 1 .. Line'Last) & ": " & Message);
488 procedure Report_Error
489 (Filename : in String;
491 At_Line : in Natural := 1)
493 Line : constant String := Natural'Image (At_Line);
495 Error_Count := Error_Count + 1;
496 if Error_Count <= Max_Error then
497 if Real_Filename = Null_Unbounded_String then
499 (Text_IO.Standard_Error, Filename & ':'
500 & Line (Line'First + 1 .. Line'Last) & ": " & Message);
503 (Text_IO.Standard_Error,
504 To_String (Real_Filename) & ':'
505 & Line (Line'First + 1 .. Line'Last) & ": " & Message);
514 function Unquote (Str : in String) return String is
515 S : String := Fixed.Trim (Str, Strings.Both);
517 if (S (S'First) = ''' and then S (S'Last) = ''')
518 or else (S (S'First) = '"' and then S (S'Last) = '"')
520 return S (S'First + 1 .. S'Last - 1);
531 procedure P (Str : in String) renames Text_IO.Put_Line;
534 P ("Style Checker " & Version.Simple);
536 P ("style_checker [-lang name] [options] file1 file2...");
537 P (" -lang : list all built-in supported languages");
538 P (" -lang NAME : following options are for this specific language");
539 P (" -abs : output absolute path name");
540 P (" -ign EXT : ignore files having EXT has extension");
541 P (" -b : no duplicate blank lines (default)");
542 P (" -B : disable duplicate blank lines check");
543 P (" -c : check for space after comment tag (default)");
544 P (" -C : disable space in comment check");
545 P (" -cp : check copyright presence");
546 P (" -cP : disable check for copyright presence (default)");
547 P (" -cy : check for copyright year");
548 P (" -cY : disbale check for copyright year (default)");
549 P (" -cf : if present a copyright line should match the"
551 P (" -cF : disable copyright pattern check");
552 P (" -d : check single comment line dot ending");
553 P (" -D : disable check for single comment line dot ending");
554 P (" -e DOS|UNIX : line ending style (UNIX default)");
555 P (" -E : disable line ending check");
556 P (" -h N : start with an header of N line (default N 20)");
557 P (" -H : disable header check");
558 P (" -l N : line length <= N (default 79)");
559 P (" -L : disable line length check");
560 P (" -m N : output only the first N errors");
561 P (" -n NAME : filename to report in error message");
562 P (" -s : syntax check (default)");
563 P (" -sp PARAM : additional parameter for the style checker");
564 P (" -S : disable syntax check");
565 P (" -t : check for trailing spaces (default)");
566 P (" -T : disable trailing spaces check");
567 P (" -v : display version");
571 Lang : Languages.Lang_Access;
574 if Ada.Command_Line.Argument_Count = 0 then
575 raise Checks.Syntax_Error;
577 elsif Ada.Command_Line.Argument_Count = 1
578 and then Ada.Command_Line.Argument (1) = "-lang"
584 case GNAT.Command_Line.Getopt
585 ("abs lang: ign: e: E l? h? H "
586 & "L b B s S t T v c? C cp cy cP cY cf: cF d D sp: m: n:")
592 if GNAT.Command_Line.Full_Switch = "abs" then
593 Absolute_Pathname := True;
595 raise Checks.Syntax_Error;
599 Languages.Set_Comment_Dot_EOL (Lang, False);
602 Languages.Set_Comment_Dot_EOL (Lang, True);
605 Languages.Set_Line_Ending
606 (Lang, Checks.Line_Ending_Style'Value
607 (GNAT.Command_Line.Parameter));
610 Languages.Set_Line_Ending (Lang, Checks.Any);
614 Full : constant String := GNAT.Command_Line.Full_Switch;
617 Ignore_Set.Include (GNAT.Command_Line.Parameter);
619 raise Checks.Syntax_Error;
625 Full : constant String := GNAT.Command_Line.Full_Switch;
627 if Full = "lang" then
628 Lang := Languages.Get_From_Name
629 (GNAT.Command_Line.Parameter);
631 elsif Full = "l" then
633 P : constant String := GNAT.Command_Line.Parameter;
636 Languages.Set_Line_Length_Max (Lang, 79);
638 Languages.Set_Line_Length_Max
639 (Lang, Positive'Value (P));
642 when Constraint_Error | IO_Exceptions.Name_Error =>
643 raise Checks.Syntax_Error;
649 Languages.Set_Line_Length_Max (Lang, Positive'Last);
653 P : constant String := GNAT.Command_Line.Parameter;
656 Languages.Set_Header_Size (Lang, 20);
658 Languages.Set_Header_Size (Lang, Positive'Value (P));
661 when Constraint_Error | IO_Exceptions.Name_Error =>
662 raise Checks.Syntax_Error;
666 Languages.Set_Header_Size (Lang, 0);
669 Languages.Set_Duplicate_Blank_Line (Lang, Checks.Rejected);
672 Languages.Set_Duplicate_Blank_Line (Lang, Checks.Accepted);
675 Languages.Set_Trailing_Spaces (Lang, Checks.Rejected);
678 Languages.Set_Trailing_Spaces (Lang, Checks.Accepted);
682 Full : constant String := GNAT.Command_Line.Full_Switch;
685 Languages.Add_Style_Checker_Parameter
686 (Lang, GNAT.Command_Line.Parameter);
689 Languages.Set_Syntax_Check (Lang, True);
694 Languages.Set_Syntax_Check (Lang, False);
698 Full : constant String := GNAT.Command_Line.Full_Switch;
702 P : constant String := GNAT.Command_Line.Parameter;
705 Languages.Set_Space_Comment (Lang, 2);
707 Languages.Set_Space_Comment
708 (Lang, Positive'Value (P));
712 elsif Full = "cp" then
713 Languages.Set_Copyright_Present (Lang, True);
715 elsif Full = "cP" then
716 Languages.Set_Copyright_Present (Lang, False);
718 elsif Full = "cy" then
719 Languages.Set_Copyright_Year (Lang, True);
721 elsif Full = "cY" then
722 Languages.Set_Copyright_Year (Lang, False);
724 elsif Full = "cf" then
725 Languages.Set_Copyright_Pattern
726 (Lang, Unquote (GNAT.Command_Line.Parameter));
728 elsif Full = "cF" then
729 Languages.Set_Copyright_Pattern (Lang, "");
734 Languages.Set_Space_Comment (Lang, 0);
737 Max_Error := Natural'Value (GNAT.Command_Line.Parameter);
741 To_Unbounded_String (GNAT.Command_Line.Parameter);
744 Text_IO.Put_Line ("Style Checker " & Version.Complete);
748 raise Checks.Syntax_Error;
752 -- Register some known extension to ignore
754 Ignore_Set.Include ("gif");
755 Ignore_Set.Include ("png");
756 Ignore_Set.Include ("jpg");
757 Ignore_Set.Include ("pdf");
758 Ignore_Set.Include ("ps");
759 Ignore_Set.Include ("exe");
760 Ignore_Set.Include ("dll");
761 Ignore_Set.Include ("so");
762 Ignore_Set.Include ("o");
763 Ignore_Set.Include ("obj");
767 Filename : constant String :=
768 GNAT.Command_Line.Get_Argument (Do_Expansion => True);
770 exit when Filename'Length = 0;
772 if Directories.Exists (Filename) then
773 if Directories.Kind (Filename) /= Directories.Directory then
775 Ext : constant String := Directories.Extension (Filename);
777 if (Ext /= "" and then not Ignore_Set.Contains (Ext))
779 (Ext = "" and then not Ignore_Set.Contains
780 (Directories.Simple_Name (Filename)))
782 -- Do not check directory
789 Report_Error (Filename, "file not found");
796 if Style_Error or else Error_Count > 0 then
797 Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);
799 Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Success);
803 when Checks.Syntax_Error | GNAT.Command_Line.Invalid_Switch =>
805 Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure);