1 package org.apache.maven.plugins.checkstyle;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.File;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.ResourceBundle;
28
29 import org.apache.commons.lang3.StringUtils;
30 import org.apache.maven.doxia.sink.Sink;
31 import org.apache.maven.doxia.sink.SinkEventAttributes;
32 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
33 import org.apache.maven.doxia.tools.SiteTool;
34 import org.apache.maven.plugin.logging.Log;
35 import org.apache.maven.plugin.logging.SystemStreamLog;
36 import org.apache.maven.plugins.checkstyle.exec.CheckstyleResults;
37
38 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
39 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
40 import com.puppycrawl.tools.checkstyle.api.Configuration;
41 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
42
43
44
45
46
47
48 public class CheckstyleReportGenerator
49 {
50 private Log log;
51
52 private final File basedir;
53
54 private final ResourceBundle bundle;
55
56 private final Sink sink;
57
58 private SeverityLevel severityLevel;
59
60 private Configuration checkstyleConfig;
61
62 private boolean enableRulesSummary;
63
64 private boolean enableSeveritySummary;
65
66 private boolean enableFilesSummary;
67
68 @Deprecated
69 private boolean enableRSS;
70
71 private final SiteTool siteTool;
72
73 private String xrefLocation;
74
75 private String xrefTestLocation;
76
77 private List<File> testSourceDirectories = new ArrayList<>();
78
79 private List<String> treeWalkerNames = Collections.singletonList( "TreeWalker" );
80
81 private final IconTool iconTool;
82
83 private final String ruleset;
84
85 public CheckstyleReportGenerator( Sink sink, ResourceBundle bundle, File basedir, SiteTool siteTool,
86 String ruleset )
87 {
88 this.bundle = bundle;
89
90 this.sink = sink;
91
92 this.basedir = basedir;
93
94 this.siteTool = siteTool;
95
96 this.ruleset = ruleset;
97
98 this.enableRulesSummary = true;
99 this.enableSeveritySummary = true;
100 this.enableFilesSummary = true;
101 this.enableRSS = false;
102 this.iconTool = new IconTool( sink, bundle );
103 }
104
105 public Log getLog()
106 {
107 if ( this.log == null )
108 {
109 this.log = new SystemStreamLog();
110 }
111 return this.log;
112 }
113
114 public void setLog( Log log )
115 {
116 this.log = log;
117 }
118
119 private String getTitle()
120 {
121 String title;
122
123 if ( getSeverityLevel() == null )
124 {
125 title = bundle.getString( "report.checkstyle.title" );
126 }
127 else
128 {
129 title = bundle.getString( "report.checkstyle.severity_title" ) + severityLevel.getName();
130 }
131
132 return title;
133 }
134
135 public void generateReport( CheckstyleResults results )
136 {
137 doHeading();
138
139 if ( getSeverityLevel() == null )
140 {
141 if ( enableSeveritySummary )
142 {
143 doSeveritySummary( results );
144 }
145
146 if ( enableFilesSummary )
147 {
148 doFilesSummary( results );
149 }
150
151 if ( enableRulesSummary )
152 {
153 doRulesSummary( results );
154 }
155 }
156
157 doDetails( results );
158 sink.body_();
159 sink.flush();
160 sink.close();
161 }
162
163 private void doHeading()
164 {
165 sink.head();
166 sink.title();
167 sink.text( getTitle() );
168 sink.title_();
169 sink.head_();
170
171 sink.body();
172
173 sink.section1();
174 sink.sectionTitle1();
175 sink.text( getTitle() );
176 sink.sectionTitle1_();
177
178 sink.paragraph();
179 sink.text( bundle.getString( "report.checkstyle.checkstylelink" ) + " " );
180 sink.link( "https://checkstyle.org/" );
181 sink.text( "Checkstyle" );
182 sink.link_();
183 String version = getCheckstyleVersion();
184 if ( version != null )
185 {
186 sink.text( " " );
187 sink.text( version );
188 }
189 sink.text( " " );
190 sink.text( String.format( bundle.getString( "report.checkstyle.ruleset" ), ruleset ) );
191 sink.text( "." );
192
193 if ( enableRSS )
194 {
195 sink.nonBreakingSpace();
196 sink.link( "checkstyle.rss" );
197 sink.figure();
198 sink.figureCaption();
199 sink.text( "rss feed" );
200 sink.figureCaption_();
201 sink.figureGraphics( "images/rss.png" );
202 sink.figure_();
203 sink.link_();
204 }
205
206 sink.paragraph_();
207 sink.section1_();
208 }
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223 private String getConfigAttribute( Configuration config, ChainedItem<Configuration> parentConfiguration,
224 String attributeName, String defaultValue )
225 {
226 String ret;
227 try
228 {
229 ret = config.getAttribute( attributeName );
230 }
231 catch ( CheckstyleException e )
232 {
233
234 if ( parentConfiguration != null )
235 {
236 ret =
237 getConfigAttribute( parentConfiguration.value, parentConfiguration.parent, attributeName,
238 defaultValue );
239 }
240 else
241 {
242 ret = defaultValue;
243 }
244 }
245 return ret;
246 }
247
248
249
250
251
252
253 private void doRulesSummary( CheckstyleResults results )
254 {
255 if ( checkstyleConfig == null )
256 {
257 return;
258 }
259
260 sink.section1();
261 sink.sectionTitle1();
262 sink.text( bundle.getString( "report.checkstyle.rules" ) );
263 sink.sectionTitle1_();
264
265 sink.table();
266 sink.tableRows( null, false );
267
268 sink.tableRow();
269 sink.tableHeaderCell();
270 sink.text( bundle.getString( "report.checkstyle.rule.category" ) );
271 sink.tableHeaderCell_();
272
273 sink.tableHeaderCell();
274 sink.text( bundle.getString( "report.checkstyle.rule" ) );
275 sink.tableHeaderCell_();
276
277 sink.tableHeaderCell();
278 sink.text( bundle.getString( "report.checkstyle.violations" ) );
279 sink.tableHeaderCell_();
280
281 sink.tableHeaderCell();
282 sink.text( bundle.getString( "report.checkstyle.column.severity" ) );
283 sink.tableHeaderCell_();
284
285 sink.tableRow_();
286
287
288 if ( "checker".equalsIgnoreCase( checkstyleConfig.getName() ) )
289 {
290 String category = null;
291 for ( ConfReference ref: sortConfiguration( results ) )
292 {
293 doRuleRow( ref, results, category );
294
295 category = ref.category;
296 }
297 }
298 else
299 {
300 sink.tableRow();
301 sink.tableCell();
302 sink.text( bundle.getString( "report.checkstyle.norule" ) );
303 sink.tableCell_();
304 sink.tableRow_();
305 }
306
307 sink.tableRows_();
308 sink.table_();
309
310 sink.section1_();
311 }
312
313
314
315
316
317
318
319
320 private void doRuleRow( ConfReference ref, CheckstyleResults results, String previousCategory )
321 {
322 Configuration checkerConfig = ref.configuration;
323 ChainedItem<Configuration> parentConfiguration = ref.parentConfiguration;
324 String ruleName = checkerConfig.getName();
325
326 sink.tableRow();
327
328
329 sink.tableCell();
330 String category = ref.category;
331 if ( !category.equals( previousCategory ) )
332 {
333 sink.text( category );
334 }
335 sink.tableCell_();
336
337
338 sink.tableCell();
339 if ( !"extension".equals( category ) )
340 {
341 sink.link( "https://checkstyle.org/config_" + category + ".html#" + ruleName );
342 sink.text( ruleName );
343 sink.link_();
344 }
345 else
346 {
347 sink.text( ruleName );
348 }
349
350 List<String> attribnames = new ArrayList<>( Arrays.asList( checkerConfig.getAttributeNames() ) );
351 attribnames.remove( "severity" );
352 if ( !attribnames.isEmpty() )
353 {
354 sink.list();
355 for ( String name : attribnames )
356 {
357 sink.listItem();
358
359 sink.text( name );
360
361 String value = getConfigAttribute( checkerConfig, null, name, "" );
362
363 if ( "header".equals( name ) && ( "Header".equals( ruleName ) || "RegexpHeader".equals( ruleName ) ) )
364 {
365 String[] lines = StringUtils.split( value, "\\n" );
366 int linenum = 1;
367 for ( String line : lines )
368 {
369 sink.lineBreak();
370 sink.rawText( "<span style=\"color: gray\">" );
371 sink.text( linenum + ":" );
372 sink.rawText( "</span>" );
373 sink.nonBreakingSpace();
374 sink.monospaced();
375 sink.text( line );
376 sink.monospaced_();
377 linenum++;
378 }
379 }
380 else if ( "headerFile".equals( name ) && "RegexpHeader".equals( ruleName ) )
381 {
382 sink.text( ": " );
383 sink.monospaced();
384 sink.text( "\"" );
385 if ( basedir != null )
386 {
387
388 String path = siteTool.getRelativePath( value, basedir.getAbsolutePath() );
389 sink.text( path.replace( '\\', '/' ) );
390 }
391 else
392 {
393 sink.text( value );
394 }
395 sink.text( "\"" );
396 sink.monospaced_();
397 }
398 else
399 {
400 sink.text( ": " );
401 sink.monospaced();
402 sink.text( "\"" );
403 sink.text( value );
404 sink.text( "\"" );
405 sink.monospaced_();
406 }
407 sink.listItem_();
408 }
409 sink.list_();
410 }
411
412 sink.tableCell_();
413
414
415 sink.tableCell();
416 sink.text( String.valueOf( ref.violations ) );
417 sink.tableCell_();
418
419
420 sink.tableCell();
421
422
423 String severity = getConfigAttribute( checkerConfig, parentConfiguration, "severity", "error" );
424 iconTool.iconSeverity( severity, IconTool.TEXT_SIMPLE );
425 sink.tableCell_();
426
427 sink.tableRow_();
428 }
429
430
431
432
433
434
435
436
437
438
439 public boolean matchRule( AuditEvent event, String ruleName, String expectedMessage, String expectedSeverity )
440 {
441 if ( !ruleName.equals( RuleUtil.getName( event ) ) )
442 {
443 return false;
444 }
445
446
447
448 if ( expectedMessage != null )
449 {
450
451
452
453 String msgWithoutSingleQuote = StringUtils.replace( expectedMessage, "'", "" );
454
455 if ( ! ( expectedMessage.equals( event.getMessage() )
456 || msgWithoutSingleQuote.equals( event.getMessage() ) ) )
457 {
458 return false;
459 }
460 }
461
462
463
464
465 if ( expectedSeverity != null )
466 {
467 if ( !expectedSeverity.equals( event.getSeverityLevel().getName() ) )
468 {
469 return false;
470 }
471 }
472 return true;
473 }
474
475 private void doSeveritySummary( CheckstyleResults results )
476 {
477 sink.section1();
478 sink.sectionTitle1();
479 sink.text( bundle.getString( "report.checkstyle.summary" ) );
480 sink.sectionTitle1_();
481
482 sink.table();
483 sink.tableRows( null, false );
484
485 sink.tableRow();
486 sink.tableHeaderCell();
487 sink.text( bundle.getString( "report.checkstyle.files" ) );
488 sink.tableHeaderCell_();
489
490 sink.tableHeaderCell();
491 iconTool.iconInfo( IconTool.TEXT_TITLE );
492 sink.tableHeaderCell_();
493
494 sink.tableHeaderCell();
495 iconTool.iconWarning( IconTool.TEXT_TITLE );
496 sink.tableHeaderCell_();
497
498 sink.tableHeaderCell();
499 iconTool.iconError( IconTool.TEXT_TITLE );
500 sink.tableHeaderCell_();
501 sink.tableRow_();
502
503 sink.tableRow();
504 sink.tableCell();
505 sink.text( String.valueOf( results.getFileCount() ) );
506 sink.tableCell_();
507 sink.tableCell();
508 sink.text( String.valueOf( results.getSeverityCount( SeverityLevel.INFO ) ) );
509 sink.tableCell_();
510 sink.tableCell();
511 sink.text( String.valueOf( results.getSeverityCount( SeverityLevel.WARNING ) ) );
512 sink.tableCell_();
513 sink.tableCell();
514 sink.text( String.valueOf( results.getSeverityCount( SeverityLevel.ERROR ) ) );
515 sink.tableCell_();
516 sink.tableRow_();
517
518 sink.tableRows_();
519 sink.table_();
520
521 sink.section1_();
522 }
523
524 private void doFilesSummary( CheckstyleResults results )
525 {
526 sink.section1();
527 sink.sectionTitle1();
528 sink.text( bundle.getString( "report.checkstyle.files" ) );
529 sink.sectionTitle1_();
530
531 sink.table();
532 sink.tableRows( null, false );
533
534 sink.tableRow();
535 sink.tableHeaderCell();
536 sink.text( bundle.getString( "report.checkstyle.file" ) );
537 sink.tableHeaderCell_();
538 sink.tableHeaderCell();
539 iconTool.iconInfo( IconTool.TEXT_ABBREV );
540 sink.tableHeaderCell_();
541 sink.tableHeaderCell();
542 iconTool.iconWarning( IconTool.TEXT_ABBREV );
543 sink.tableHeaderCell_();
544 sink.tableHeaderCell();
545 iconTool.iconError( IconTool.TEXT_ABBREV );
546 sink.tableHeaderCell_();
547 sink.tableRow_();
548
549
550 List<String> fileList = new ArrayList<>( results.getFiles().keySet() );
551 Collections.sort( fileList );
552
553 for ( String filename : fileList )
554 {
555 List<AuditEvent> violations = results.getFileViolations( filename );
556 if ( violations.isEmpty() )
557 {
558
559 continue;
560 }
561
562 sink.tableRow();
563
564 sink.tableCell();
565 sink.link( "#" + filename.replace( '/', '.' ) );
566 sink.text( filename );
567 sink.link_();
568 sink.tableCell_();
569
570 sink.tableCell();
571 sink.text( String.valueOf( results.getSeverityCount( violations, SeverityLevel.INFO ) ) );
572 sink.tableCell_();
573
574 sink.tableCell();
575 sink.text( String.valueOf( results.getSeverityCount( violations, SeverityLevel.WARNING ) ) );
576 sink.tableCell_();
577
578 sink.tableCell();
579 sink.text( String.valueOf( results.getSeverityCount( violations, SeverityLevel.ERROR ) ) );
580 sink.tableCell_();
581
582 sink.tableRow_();
583 }
584
585 sink.tableRows_();
586 sink.table_();
587
588 sink.section1_();
589 }
590
591 private void doDetails( CheckstyleResults results )
592 {
593
594 sink.section1();
595 sink.sectionTitle1();
596 sink.text( bundle.getString( "report.checkstyle.details" ) );
597 sink.sectionTitle1_();
598
599
600 List<String> fileList = new ArrayList<>( results.getFiles().keySet() );
601 Collections.sort( fileList );
602
603 for ( String file : fileList )
604 {
605 List<AuditEvent> violations = results.getFileViolations( file );
606
607 if ( violations.isEmpty() )
608 {
609
610 continue;
611 }
612
613 sink.section2();
614 SinkEventAttributes attrs = new SinkEventAttributeSet();
615 attrs.addAttribute( SinkEventAttributes.ID, file.replace( '/', '.' ) );
616 sink.sectionTitle( Sink.SECTION_LEVEL_2, attrs );
617 sink.text( file );
618 sink.sectionTitle_( Sink.SECTION_LEVEL_2 );
619
620 sink.table();
621 sink.tableRows( null, false );
622
623 sink.tableRow();
624 sink.tableHeaderCell();
625 sink.text( bundle.getString( "report.checkstyle.column.severity" ) );
626 sink.tableHeaderCell_();
627 sink.tableHeaderCell();
628 sink.text( bundle.getString( "report.checkstyle.rule.category" ) );
629 sink.tableHeaderCell_();
630 sink.tableHeaderCell();
631 sink.text( bundle.getString( "report.checkstyle.rule" ) );
632 sink.tableHeaderCell_();
633 sink.tableHeaderCell();
634 sink.text( bundle.getString( "report.checkstyle.column.message" ) );
635 sink.tableHeaderCell_();
636 sink.tableHeaderCell();
637 sink.text( bundle.getString( "report.checkstyle.column.line" ) );
638 sink.tableHeaderCell_();
639 sink.tableRow_();
640
641 doFileEvents( violations, file );
642
643 sink.tableRows_();
644 sink.table_();
645
646 sink.section2_();
647 }
648
649 sink.section1_();
650 }
651
652 private void doFileEvents( List<AuditEvent> eventList, String filename )
653 {
654 for ( AuditEvent event : eventList )
655 {
656 SeverityLevel level = event.getSeverityLevel();
657
658 if ( ( getSeverityLevel() != null ) && !( getSeverityLevel() != level ) )
659 {
660 continue;
661 }
662
663 sink.tableRow();
664
665 sink.tableCell();
666 iconTool.iconSeverity( level.getName(), IconTool.TEXT_SIMPLE );
667 sink.tableCell_();
668
669 sink.tableCell();
670 String category = RuleUtil.getCategory( event );
671 if ( category != null )
672 {
673 sink.text( category );
674 }
675 sink.tableCell_();
676
677 sink.tableCell();
678 String ruleName = RuleUtil.getName( event );
679 if ( ruleName != null )
680 {
681 sink.text( ruleName );
682 }
683 sink.tableCell_();
684
685 sink.tableCell();
686 sink.text( event.getMessage() );
687 sink.tableCell_();
688
689 sink.tableCell();
690
691 int line = event.getLine();
692 String effectiveXrefLocation = getEffectiveXrefLocation( eventList );
693 if ( effectiveXrefLocation != null && line != 0 )
694 {
695 sink.link( effectiveXrefLocation + "/" + filename.replaceAll( "\\.java$", ".html" ) + "#L"
696 + line );
697 sink.text( String.valueOf( line ) );
698 sink.link_();
699 }
700 else if ( line != 0 )
701 {
702 sink.text( String.valueOf( line ) );
703 }
704 sink.tableCell_();
705
706 sink.tableRow_();
707 }
708 }
709
710 private String getEffectiveXrefLocation( List<AuditEvent> eventList )
711 {
712 String absoluteFilename = eventList.get( 0 ).getFileName();
713 if ( isTestSource( absoluteFilename ) )
714 {
715 return getXrefTestLocation();
716 }
717 else
718 {
719 return getXrefLocation();
720 }
721 }
722
723 private boolean isTestSource( final String absoluteFilename )
724 {
725 for ( File testSourceDirectory : testSourceDirectories )
726 {
727 if ( absoluteFilename.startsWith( testSourceDirectory.getAbsolutePath() ) )
728 {
729 return true;
730 }
731 }
732
733 return false;
734 }
735
736 public SeverityLevel getSeverityLevel()
737 {
738 return severityLevel;
739 }
740
741 public void setSeverityLevel( SeverityLevel severityLevel )
742 {
743 this.severityLevel = severityLevel;
744 }
745
746 public boolean isEnableRulesSummary()
747 {
748 return enableRulesSummary;
749 }
750
751 public void setEnableRulesSummary( boolean enableRulesSummary )
752 {
753 this.enableRulesSummary = enableRulesSummary;
754 }
755
756 public boolean isEnableSeveritySummary()
757 {
758 return enableSeveritySummary;
759 }
760
761 public void setEnableSeveritySummary( boolean enableSeveritySummary )
762 {
763 this.enableSeveritySummary = enableSeveritySummary;
764 }
765
766 public boolean isEnableFilesSummary()
767 {
768 return enableFilesSummary;
769 }
770
771 public void setEnableFilesSummary( boolean enableFilesSummary )
772 {
773 this.enableFilesSummary = enableFilesSummary;
774 }
775
776 @Deprecated
777 public boolean isEnableRSS()
778 {
779 return enableRSS;
780 }
781
782 @Deprecated
783 public void setEnableRSS( boolean enableRSS )
784 {
785 this.enableRSS = enableRSS;
786 }
787
788 public String getXrefLocation()
789 {
790 return xrefLocation;
791 }
792
793 public void setXrefLocation( String xrefLocation )
794 {
795 this.xrefLocation = xrefLocation;
796 }
797
798 public String getXrefTestLocation()
799 {
800 return xrefTestLocation;
801 }
802
803 public void setXrefTestLocation( String xrefTestLocation )
804 {
805 this.xrefTestLocation = xrefTestLocation;
806 }
807
808 public List<File> getTestSourceDirectories()
809 {
810 return testSourceDirectories;
811 }
812
813 public void setTestSourceDirectories( List<File> testSourceDirectories )
814 {
815 this.testSourceDirectories = testSourceDirectories;
816 }
817
818 public Configuration getCheckstyleConfig()
819 {
820 return checkstyleConfig;
821 }
822
823 public void setCheckstyleConfig( Configuration config )
824 {
825 this.checkstyleConfig = config;
826 }
827
828 public void setTreeWalkerNames( List<String> treeWalkerNames )
829 {
830 this.treeWalkerNames = treeWalkerNames;
831 }
832
833 public List<String> getTreeWalkerNames()
834 {
835 return treeWalkerNames;
836 }
837
838
839
840
841
842 private String getCheckstyleVersion()
843 {
844 Package checkstyleApiPackage = Configuration.class.getPackage();
845
846 return ( checkstyleApiPackage == null ) ? null : checkstyleApiPackage.getImplementationVersion();
847 }
848
849 public List<ConfReference> sortConfiguration( CheckstyleResults results )
850 {
851 List<ConfReference> result = new ArrayList<>();
852
853 sortConfiguration( result, checkstyleConfig, null, results );
854
855 Collections.sort( result );
856
857 return result;
858 }
859
860 private void sortConfiguration( List<ConfReference> result, Configuration config,
861 ChainedItem<Configuration> parent, CheckstyleResults results )
862 {
863 for ( Configuration childConfig : config.getChildren() )
864 {
865 String ruleName = childConfig.getName();
866
867 if ( treeWalkerNames.contains( ruleName ) )
868 {
869
870 sortConfiguration( result, childConfig, new ChainedItem<>( config, parent ), results );
871 }
872 else
873 {
874 String fixedmessage = getConfigAttribute( childConfig, null, "message", null );
875
876
877
878 String configSeverity = getConfigAttribute( childConfig, null, "severity", null );
879
880
881 long violations = 0;
882 AuditEvent lastMatchedEvent = null;
883 for ( List<AuditEvent> errors : results.getFiles().values() )
884 {
885 for ( AuditEvent event : errors )
886 {
887 if ( matchRule( event, ruleName, fixedmessage, configSeverity ) )
888 {
889 lastMatchedEvent = event;
890 violations++;
891 }
892 }
893 }
894
895 if ( violations > 0 )
896 {
897 String category = RuleUtil.getCategory( lastMatchedEvent );
898
899 result.add( new ConfReference( category, childConfig, parent, violations, result.size() ) );
900 }
901 }
902 }
903 }
904
905 private static class ConfReference
906 implements Comparable<ConfReference>
907 {
908 private final String category;
909 private final Configuration configuration;
910 private final ChainedItem<Configuration> parentConfiguration;
911 private final long violations;
912 private final int count;
913
914 ConfReference( String category, Configuration configuration,
915 ChainedItem<Configuration> parentConfiguration, long violations, int count )
916 {
917 this.category = category;
918 this.configuration = configuration;
919 this.parentConfiguration = parentConfiguration;
920 this.violations = violations;
921 this.count = count;
922 }
923
924 public int compareTo( ConfReference o )
925 {
926 int compare = category.compareTo( o.category );
927 if ( compare == 0 )
928 {
929 compare = configuration.getName().compareTo( o.configuration.getName() );
930 }
931 return ( compare == 0 ) ? ( o.count - count ) : compare;
932 }
933 }
934
935 private static class ChainedItem<T>
936 {
937 private final ChainedItem<T> parent;
938
939 private final T value;
940
941 ChainedItem( T value, ChainedItem<T> parent )
942 {
943 this.parent = parent;
944 this.value = value;
945 }
946 }
947
948 }