1 ------------------------------------------------------------------------------
4 -- Copyright (C) 2006-2010, 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 Multiline_Comment
: Boolean := False;
94 Consecutive_Comment
: Natural := 0;
95 Last_Comment_Dot_EOL
: Boolean := False;
98 procedure Check
(Filename
: in String);
102 (Checker
: in out File_Checker
;
104 Line_Ending
: in Checks
.Line_Ending_Style
);
105 -- Pass all checks that are line related
107 subtype Line_Offset
is Integer range -1 .. 0;
109 procedure Report_Error
110 (File
: in File_Reader
.File_Type
;
112 Offset
: in Line_Offset
:= 0);
113 -- Report an error to standard error
115 procedure Report_Error
116 (Filename
: in String;
118 At_Line
: in Natural := 1);
119 -- Report an error to standard error
122 -- Display the usage information
124 procedure List_Languages
;
125 -- Display supported languages
127 function Unquote
(Str
: in String) return String;
128 -- Removes leading/trailing spaces and quote if present
134 procedure Check
(Filename
: in String) is
135 Checker
: File_Checker
;
136 Line
: String (1 .. 2_048
);
138 Nb_Line
: Natural := 0;
139 Ending
: Checks
.Line_Ending_Style
;
141 Checker
.Lang
:= new Languages
.Lang
'Class'(Languages.Get (Filename));
143 -- Run line oriented tests
145 File_Reader.Open (Checker.File, Filename);
147 while not File_Reader.End_Of_File (Checker.File) loop
148 File_Reader.Get_Line (Checker.File, Line, K, Ending);
149 Check_Line (Checker, Line (1 .. K), Ending);
152 Nb_Line := File_Reader.Line (Checker.File);
154 File_Reader.Close (Checker.File);
156 -- Run file oriented tests
158 if Checker.Lang.Get_Syntax_Check then
159 if not Languages.Run_Syntax_Check (Checker.Lang.all, Filename) then
164 if Checker.Lang.Get_Header_Size > Checker.Header_Size then
165 if Checker.Header_Size = 0 then
167 (Filename, "missing file header (must start on first line)");
170 (Filename, "file header should have at least"
171 & Positive'Image (Checker.Lang.Get_Header_Size)
172 & " lines, found" & Integer'Image (Checker.Header_Size));
176 if Checker.Lang.Get_Copyright_Present
177 and then not Checker.Copyright_Found
179 Report_Error (Filename, "missing copyright notice");
182 if Checker.Copyright_Found
183 and then Checker.Lang.Get_Copyright_Year
184 and then not Checker.Copyright_Year
187 (Filename, "missing year " & Current_Year & " in copyright");
190 if Checker.Lang.Get_Duplicate_Blank_Line = Checks.Rejected
191 and then Checker.Count_Blank >= 1
194 (Filename => Filename,
195 Message => "blank line not allowed at end of file",
200 when IO_Exceptions.Name_Error =>
201 Report_Error (Filename, "can't open file");
209 (Checker : in out File_Checker;
211 Line_Ending : in Checks.Line_Ending_Style)
213 procedure Check_Ending;
215 procedure Check_Length_Max;
217 procedure Check_Duplicate_Blank;
219 procedure Check_Trailing_Spaces;
221 procedure Check_Header;
223 procedure Check_Copyright;
225 procedure Check_Space_Comment;
227 procedure Check_Comment_Dot_EOL;
231 ---------------------------
232 -- Check_Comment_Dot_EOL --
233 ---------------------------
235 procedure Check_Comment_Dot_EOL is
238 if not Checker.Lang.Get_Comment_Dot_EOL
239 and then Checker.Lang.Comment /= ""
241 if Fixed.Index (Line, String'(Checker
.Lang
.Comment
)) /= 0 then
243 Checker
.Consecutive_Comment
:= Checker
.Consecutive_Comment
+ 1;
245 Pos
:= Fixed
.Index_Non_Blank
(Line
, Going
=> Backward
);
248 and then Pos
> Line
'First + 1
249 and then Line
(Pos
- 2 .. Pos
- 1) /= ".."
251 Checker
.Last_Comment_Dot_EOL
:= True;
253 Checker
.Last_Comment_Dot_EOL
:= False;
257 -- No more in a comment line
259 if Checker
.Consecutive_Comment
= 1
260 and then Checker
.Last_Comment_Dot_EOL
264 "single line comment should not terminate with dot",
268 Checker
.Consecutive_Comment
:= 0;
269 Checker
.Last_Comment_Dot_EOL
:= False;
272 end Check_Comment_Dot_EOL
;
274 ---------------------
275 -- Check_Copyright --
276 ---------------------
278 procedure Check_Copyright
is
280 Co_Start
: Natural := 0;
281 Cp_Start
: Natural := Fixed
.Index
(Line
, " Copyright");
283 if Checker
.Lang
.Comment
/= "" then
284 Co_Start
:= Fixed
.Index
(Line
, String'(Checker.Lang.Comment));
288 and then Cp_Start + 10 <= Line'Length
289 and then Line (Cp_Start + 10) /= ' '
291 -- We are not at the end of the line and no space after Copyright
295 if (Checker.Lang.Get_Copyright_Present
296 or else Checker.Lang.Get_Copyright_Year)
297 and then Cp_Start /= 0
298 and then Co_Start /= 0
299 and then Cp_Start > Co_Start
301 Checker.Copyright_Found := True;
303 if Checker.Lang.Get_Copyright_Year then
304 if Fixed.Index (Line, Current_Year) /= 0 then
305 Checker.Copyright_Year := True;
310 -- Check that the copyright year follow the given regexp
313 and then Checker.Lang.Get_Copyright_Pattern /= ""
316 Pattern : constant Regpat.Pattern_Matcher :=
317 Regpat.Compile (Checker.Lang.Get_Copyright_Pattern);
319 if not Regpat.Match (Pattern, Line) then
322 "copyright line not matching expected pattern");
328 ---------------------------
329 -- Check_Duplicate_Blank --
330 ---------------------------
332 procedure Check_Duplicate_Blank is
334 if Checker.Lang.Get_Duplicate_Blank_Line = Checks.Rejected
335 and then (Line'Length = 0
336 or else Fixed.Count (Line, " " & ASCII.HT) = Line'Length)
338 Checker.Count_Blank := Checker.Count_Blank + 1;
340 if Checker.Count_Blank > 1 then
341 Report_Error (Checker.File, "duplicate blank line");
345 Checker.Count_Blank := 0;
347 end Check_Duplicate_Blank;
353 procedure Check_Ending is
355 if Checker.Lang.Get_Line_Ending /= Checks.Any then
356 if Line_Ending = Checks.No then
359 "missing line terminator");
360 elsif Checker.Lang.Get_Line_Ending /= Line_Ending then
363 "wrong " & Checks.Line_Ending_Style'Image (Line_Ending) &
373 procedure Check_Header is
374 C : constant String := Checker.Lang.Comment;
375 CS : constant String := Checker.Lang.Start_Multiline_Comment;
376 CE : constant String := Checker.Lang.End_Multiline_Comment;
377 Is_C : constant Boolean :=
379 and then Line'Length >= C'Length
381 (Line'First .. Line'First + C'Length - 1) = C;
382 Is_CS : constant Boolean :=
384 and then File_Reader.Line (Checker.File) = 1
385 and then Line'Length >= CS'Length
387 (Line'First .. Line'First + CS'Length - 1) = CS;
388 Is_CE : constant Boolean :=
390 and then Line'Length >= CE'Length
392 (Line'Last - CE'Length + 1 .. Line'Last) = CE;
394 -- Check that we are starting with a multi-line comment
396 if File_Reader.Line (Checker.File) = 1 then
397 if Is_C or else Is_CS then
398 Checker.Header_Size := Checker.Header_Size + 1;
401 Checker.Multiline_Comment := True;
405 Checker.In_Header := False;
411 (Is_C or else (Checker.Multiline_Comment and then not Is_CE))
413 Checker.Header_Size := Checker.Header_Size + 1;
416 Checker.Header_Size := Checker.Header_Size + 1;
418 Checker.In_Header := False;
423 ----------------------
424 -- Check_Length_Max --
425 ----------------------
427 procedure Check_Length_Max is
429 if Line'Length > Checker.Lang.Get_Line_Length_Max then
430 Report_Error (Checker.File, "line too long");
432 end Check_Length_Max;
434 -------------------------
435 -- Check_Space_Comment --
436 -------------------------
438 procedure Check_Space_Comment is
439 N : constant Natural := Checker.Lang.Get_Space_Comment;
440 NI : constant String := Natural'Image (N);
441 C : constant String := Checker.Lang.Comment;
442 I : constant Natural := Fixed.Index_Non_Blank (Line);
446 and then I + C'Length - 1 <= Line'Last
447 and then Line (I .. I + C'Length - 1) = C
448 and then Line (Line'Last - C'Length + 1 .. Line'Last) /= C
449 and then (Line (I .. I + 1) /= "#!"
450 or else File_Reader.Line (Checker.File) > 1)
451 -- Do no check script headers
453 for K in I + C'Length .. I + C'Length + N - 1 loop
454 if Line (K) /= ' ' then
457 NI (NI'First + 1 .. NI'Last) & " spaces after " & C);
462 end Check_Space_Comment;
468 procedure Check_Tab is
470 if Checker.Lang.Get_Tabulation = Checks.Rejected
471 and then Strings.Fixed.Index (Line, String'(1 => ASCII
.HT
)) /= 0
473 Report_Error
(Checker
.File
, "no tabulations allowed");
477 ---------------------------
478 -- Check_Trailing_Spaces --
479 ---------------------------
481 procedure Check_Trailing_Spaces
is
483 if Checker
.Lang
.Get_Trailing_Spaces
= Checks
.Rejected
484 and then Line
'Length > 0
485 and then (Line
(Line
'Last) = ' '
486 or else Line
(Line
'Last) = ASCII
.HT
)
488 Report_Error
(Checker
.File
, "no trailing spaces allowed");
490 end Check_Trailing_Spaces
;
495 Check_Duplicate_Blank
;
496 Check_Trailing_Spaces
;
500 Check_Comment_Dot_EOL
;
508 procedure List_Languages
is
509 procedure P
(Str
: in String) renames Text_IO
.Put_Line
;
512 P
("Style Checker " & Version
.Simple
);
522 procedure Report_Error
523 (File
: in File_Reader
.File_Type
;
525 Offset
: in Line_Offset
:= 0)
527 Line
: constant String :=
528 Natural'Image (File_Reader
.Line
(File
) + Offset
);
530 Error_Count
:= Error_Count
+ 1;
531 if Error_Count
<= Max_Error
then
532 if Real_Filename
= Null_Unbounded_String
then
534 (Text_IO
.Standard_Error
,
535 File_Reader
.Name
(File
, Absolute_Pathname
) & ':'
536 & Line
(Line
'First + 1 .. Line
'Last) & ": " & Message
);
539 (Text_IO
.Standard_Error
,
540 To_String
(Real_Filename
) & ':'
541 & Line
(Line
'First + 1 .. Line
'Last) & ": " & Message
);
546 procedure Report_Error
547 (Filename
: in String;
549 At_Line
: in Natural := 1)
551 Line
: constant String := Natural'Image (At_Line
);
553 Error_Count
:= Error_Count
+ 1;
554 if Error_Count
<= Max_Error
then
555 if Real_Filename
= Null_Unbounded_String
then
557 (Text_IO
.Standard_Error
, Filename
& ':'
558 & Line
(Line
'First + 1 .. Line
'Last) & ": " & Message
);
561 (Text_IO
.Standard_Error
,
562 To_String
(Real_Filename
) & ':'
563 & Line
(Line
'First + 1 .. Line
'Last) & ": " & Message
);
572 function Unquote
(Str
: in String) return String is
573 S
: constant String := Fixed
.Trim
(Str
, Strings
.Both
);
575 if (S
(S
'First) = ''' and then S
(S
'Last) = ''')
576 or else (S
(S
'First) = '"' and then S
(S
'Last) = '"')
578 return S
(S
'First + 1 .. S
'Last - 1);
589 procedure P
(Str
: in String) renames Text_IO
.Put_Line
;
592 P
("Style Checker " & Version
.Simple
);
594 P
("style_checker [-lang name] [options] file1 file2...");
595 P
(" -lang : list all built-in supported languages");
596 P
(" -lang NAME : following options are for this specific language");
597 P
(" -a : check for tabulations (default)");
598 P
(" -A : disable tabulations check");
599 P
(" -abs : output absolute path name");
600 P
(" -ign EXT : ignore files having EXT has extension");
601 P
(" -b : no duplicate blank lines (default)");
602 P
(" -B : disable duplicate blank lines check");
603 P
(" -c : check for space after comment tag (default)");
604 P
(" -C : disable space in comment check");
605 P
(" -cp : check copyright presence");
606 P
(" -cP : disable check for copyright presence (default)");
607 P
(" -cy : check for copyright year");
608 P
(" -cY : disable check for copyright year (default)");
609 P
(" -cf : if present a copyright line should match the"
611 P
(" -cF : disable copyright pattern check");
612 P
(" -d : check single comment line dot ending");
613 P
(" -D : disable check for single comment line dot"
614 & " ending (default)");
615 P
(" -e DOS|UNIX : line ending style (UNIX default)");
616 P
(" -E : disable line ending check");
617 P
(" -h N : start with an header of N line (default N 20)");
618 P
(" -H : disable header check");
619 P
(" -l N : line length <= N (default 79)");
620 P
(" -L : disable line length check");
621 P
(" -m N : output only the first N errors");
622 P
(" -n NAME : filename to report in error message");
623 P
(" -s : syntax check (default)");
624 P
(" -sp PARAM : additional parameter for the style checker");
625 P
(" -S : disable syntax check");
626 P
(" -t : check for trailing spaces (default)");
627 P
(" -T : disable trailing spaces check");
628 P
(" -v : display version");
632 Lang
: Languages
.Lang_Access
;
635 if Ada
.Command_Line
.Argument_Count
= 0 then
636 raise Checks
.Syntax_Error
;
638 elsif Ada
.Command_Line
.Argument_Count
= 1
639 and then Ada
.Command_Line
.Argument
(1) = "-lang"
643 elsif Ada
.Command_Line
.Argument_Count
= 1
644 and then Ada
.Command_Line
.Argument
(1) = "-h"
647 Ada
.Command_Line
.Set_Exit_Status
(Ada
.Command_Line
.Failure
);
651 case GNAT
.Command_Line
.Getopt
652 ("a A abs lang: ign: e: E l? h? H "
653 & "L b B s S t T v c? C cp cy cP cY cf: cF d D sp: m: n:")
659 if GNAT
.Command_Line
.Full_Switch
= "abs" then
660 Absolute_Pathname
:= True;
662 elsif GNAT
.Command_Line
.Full_Switch
= "a" then
663 Languages
.Set_Tabulation
(Lang
, Checks
.Rejected
);
666 raise Checks
.Syntax_Error
;
670 Languages
.Set_Tabulation
(Lang
, Checks
.Accepted
);
673 Languages
.Set_Comment_Dot_EOL
(Lang
, False);
676 Languages
.Set_Comment_Dot_EOL
(Lang
, True);
679 Languages
.Set_Line_Ending
680 (Lang
, Checks
.Line_Ending_Style
'Value
681 (GNAT
.Command_Line
.Parameter
));
684 Languages
.Set_Line_Ending
(Lang
, Checks
.Any
);
688 Full
: constant String := GNAT
.Command_Line
.Full_Switch
;
691 Ignore_Set
.Include
(GNAT
.Command_Line
.Parameter
);
693 raise Checks
.Syntax_Error
;
699 Full
: constant String := GNAT
.Command_Line
.Full_Switch
;
701 if Full
= "lang" then
702 Lang
:= Languages
.Get_From_Name
703 (GNAT
.Command_Line
.Parameter
);
705 elsif Full
= "l" then
707 P
: constant String := GNAT
.Command_Line
.Parameter
;
710 Languages
.Set_Line_Length_Max
(Lang
, 79);
712 Languages
.Set_Line_Length_Max
713 (Lang
, Positive'Value (P
));
716 when Constraint_Error | IO_Exceptions
.Name_Error
=>
717 raise Checks
.Syntax_Error
;
723 Languages
.Set_Line_Length_Max
(Lang
, Positive'Last);
727 P
: constant String := GNAT
.Command_Line
.Parameter
;
730 Languages
.Set_Header_Size
(Lang
, 20);
732 Languages
.Set_Header_Size
(Lang
, Positive'Value (P
));
735 when Constraint_Error | IO_Exceptions
.Name_Error
=>
736 raise Checks
.Syntax_Error
;
740 Languages
.Set_Header_Size
(Lang
, 0);
743 Languages
.Set_Duplicate_Blank_Line
(Lang
, Checks
.Rejected
);
746 Languages
.Set_Duplicate_Blank_Line
(Lang
, Checks
.Accepted
);
749 Languages
.Set_Trailing_Spaces
(Lang
, Checks
.Rejected
);
752 Languages
.Set_Trailing_Spaces
(Lang
, Checks
.Accepted
);
756 Full
: constant String := GNAT
.Command_Line
.Full_Switch
;
759 Languages
.Add_Style_Checker_Parameter
760 (Lang
, GNAT
.Command_Line
.Parameter
);
763 Languages
.Set_Syntax_Check
(Lang
, True);
768 Languages
.Set_Syntax_Check
(Lang
, False);
772 Full
: constant String := GNAT
.Command_Line
.Full_Switch
;
776 P
: constant String := GNAT
.Command_Line
.Parameter
;
779 Languages
.Set_Space_Comment
(Lang
, 2);
781 Languages
.Set_Space_Comment
782 (Lang
, Positive'Value (P
));
786 elsif Full
= "cp" then
787 Languages
.Set_Copyright_Present
(Lang
, True);
789 elsif Full
= "cP" then
790 Languages
.Set_Copyright_Present
(Lang
, False);
792 elsif Full
= "cy" then
793 Languages
.Set_Copyright_Year
(Lang
, True);
795 elsif Full
= "cY" then
796 Languages
.Set_Copyright_Year
(Lang
, False);
798 elsif Full
= "cf" then
799 Languages
.Set_Copyright_Pattern
800 (Lang
, Unquote
(GNAT
.Command_Line
.Parameter
));
802 elsif Full
= "cF" then
803 Languages
.Set_Copyright_Pattern
(Lang
, "");
808 Languages
.Set_Space_Comment
(Lang
, 0);
811 Max_Error
:= Natural'Value (GNAT
.Command_Line
.Parameter
);
815 To_Unbounded_String
(GNAT
.Command_Line
.Parameter
);
818 Text_IO
.Put_Line
("Style Checker " & Version
.Complete
);
822 raise Checks
.Syntax_Error
;
826 -- Register some known extension to ignore
828 Ignore_Set
.Include
("gif");
829 Ignore_Set
.Include
("png");
830 Ignore_Set
.Include
("jpg");
831 Ignore_Set
.Include
("pdf");
832 Ignore_Set
.Include
("ps");
833 Ignore_Set
.Include
("exe");
834 Ignore_Set
.Include
("dll");
835 Ignore_Set
.Include
("so");
836 Ignore_Set
.Include
("o");
837 Ignore_Set
.Include
("obj");
838 Ignore_Set
.Include
("tar");
839 Ignore_Set
.Include
("gz");
840 Ignore_Set
.Include
("bz2");
841 Ignore_Set
.Include
("7z");
845 Filename
: constant String :=
846 GNAT
.Command_Line
.Get_Argument
(Do_Expansion
=> True);
848 exit when Filename
'Length = 0;
850 if Directories
.Exists
(Filename
) then
851 if Directories
.Kind
(Filename
) /= Directories
.Directory
then
853 Ext
: constant String := Directories
.Extension
(Filename
);
855 if (Ext
/= "" and then not Ignore_Set
.Contains
(Ext
))
857 (Ext
= "" and then not Ignore_Set
.Contains
858 (Directories
.Simple_Name
(Filename
)))
860 -- Do not check directory
867 Report_Error
(Filename
, "file not found");
874 if Style_Error
or else Error_Count
> 0 then
875 Ada
.Command_Line
.Set_Exit_Status
(Ada
.Command_Line
.Failure
);
877 Ada
.Command_Line
.Set_Exit_Status
(Ada
.Command_Line
.Success
);
881 when Checks
.Syntax_Error | GNAT
.Command_Line
.Invalid_Switch
=>
883 Ada
.Command_Line
.Set_Exit_Status
(Ada
.Command_Line
.Failure
);