blob: d179e8179d6aeefb51e4b686e8273e1822e10fc7 [file] [log] [blame]
package org.apache.maven.plugins.checkstyle;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ResourceBundle;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributeSet;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.tools.SiteTool;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.apache.maven.plugins.checkstyle.exec.CheckstyleResults;
import org.codehaus.plexus.util.StringUtils;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
/**
* Generate a report based on CheckstyleResults.
*
*
*/
public class CheckstyleReportGenerator
{
private Log log;
private final File basedir;
private final ResourceBundle bundle;
private final Sink sink;
private SeverityLevel severityLevel;
private Configuration checkstyleConfig;
private boolean enableRulesSummary;
private boolean enableSeveritySummary;
private boolean enableFilesSummary;
private boolean skipEmptyViolationRules;
private boolean enableRSS;
private final SiteTool siteTool;
private String xrefLocation;
private List<String> treeWalkerNames = Collections.singletonList( "TreeWalker" );
private final IconTool iconTool;
private final String ruleset;
public CheckstyleReportGenerator( Sink sink, ResourceBundle bundle, File basedir, SiteTool siteTool,
String ruleset )
{
this.bundle = bundle;
this.sink = sink;
this.basedir = basedir;
this.siteTool = siteTool;
this.ruleset = ruleset;
this.enableRulesSummary = true;
this.enableSeveritySummary = true;
this.enableFilesSummary = true;
this.enableRSS = true;
this.iconTool = new IconTool( sink, bundle );
}
public Log getLog()
{
if ( this.log == null )
{
this.log = new SystemStreamLog();
}
return this.log;
}
public void setLog( Log log )
{
this.log = log;
}
private String getTitle()
{
String title;
if ( getSeverityLevel() == null )
{
title = bundle.getString( "report.checkstyle.title" );
}
else
{
title = bundle.getString( "report.checkstyle.severity_title" ) + severityLevel.getName();
}
return title;
}
public void generateReport( CheckstyleResults results )
{
doHeading();
if ( getSeverityLevel() == null )
{
if ( enableSeveritySummary )
{
doSeveritySummary( results );
}
if ( enableFilesSummary )
{
doFilesSummary( results );
}
if ( enableRulesSummary )
{
doRulesSummary( results );
}
}
doDetails( results );
sink.body_();
sink.flush();
sink.close();
}
private void doHeading()
{
sink.head();
sink.title();
sink.text( getTitle() );
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.sectionTitle1();
sink.text( getTitle() );
sink.sectionTitle1_();
sink.paragraph();
sink.text( bundle.getString( "report.checkstyle.checkstylelink" ) + " " );
sink.link( "http://checkstyle.sourceforge.net/" );
sink.text( "Checkstyle" );
sink.link_();
String version = getCheckstyleVersion();
if ( version != null )
{
sink.text( " " );
sink.text( version );
}
sink.text( " " );
sink.text( String.format( bundle.getString( "report.checkstyle.ruleset" ), ruleset ) );
sink.text( "." );
if ( enableRSS )
{
sink.nonBreakingSpace();
sink.link( "checkstyle.rss" );
sink.figure();
sink.figureCaption();
sink.text( "rss feed" );
sink.figureCaption_();
sink.figureGraphics( "images/rss.png" );
sink.figure_();
sink.link_();
}
sink.paragraph_();
sink.section1_();
}
/**
* Get the value of the specified attribute from the Checkstyle configuration.
* If parentConfigurations is non-null and non-empty, the parent
* configurations are searched if the attribute cannot be found in the
* current configuration. If the attribute is still not found, the
* specified default value will be returned.
*
* @param config The current Checkstyle configuration
* @param parentConfiguration The configuration of the parent of the current configuration
* @param attributeName The name of the attribute
* @param defaultValue The default value to use if the attribute cannot be found in any configuration
* @return The value of the specified attribute
*/
private String getConfigAttribute( Configuration config, ChainedItem<Configuration> parentConfiguration,
String attributeName, String defaultValue )
{
String ret;
try
{
ret = config.getAttribute( attributeName );
}
catch ( CheckstyleException e )
{
// Try to find the attribute in a parent, if there are any
if ( parentConfiguration != null )
{
ret =
getConfigAttribute( parentConfiguration.value, parentConfiguration.parent, attributeName,
defaultValue );
}
else
{
ret = defaultValue;
}
}
return ret;
}
/**
* Create the rules summary section of the report.
*
* @param results The results to summarize
*/
private void doRulesSummary( CheckstyleResults results )
{
if ( checkstyleConfig == null )
{
return;
}
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.checkstyle.rules" ) );
sink.sectionTitle1_();
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.rule.category" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.rule" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.violations" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.column.severity" ) );
sink.tableHeaderCell_();
sink.tableRow_();
// Top level should be the checker.
if ( "checker".equalsIgnoreCase( checkstyleConfig.getName() ) )
{
String category = null;
for ( ConfReference ref: sortConfiguration( results ) )
{
doRuleRow( ref, results, category );
category = ref.category;
}
}
else
{
sink.tableRow();
sink.tableCell();
sink.text( bundle.getString( "report.checkstyle.norule" ) );
sink.tableCell_();
sink.tableRow_();
}
sink.table_();
sink.section1_();
}
/**
* Create a summary for one Checkstyle rule.
*
* @param ref The configuration reference for the row
* @param results The results to summarize
* @param previousCategory The previous row's category
*/
private void doRuleRow( ConfReference ref, CheckstyleResults results, String previousCategory )
{
Configuration checkerConfig = ref.configuration;
ChainedItem<Configuration> parentConfiguration = ref.parentConfiguration;
String ruleName = checkerConfig.getName();
sink.tableRow();
// column 1: rule category
sink.tableCell();
String category = ref.category;
if ( !category.equals( previousCategory ) )
{
sink.text( category );
}
sink.tableCell_();
// column 2: Rule name + configured attributes
sink.tableCell();
if ( !"extension".equals( category ) )
{
sink.link( "http://checkstyle.sourceforge.net/config_" + category + ".html#" + ruleName );
sink.text( ruleName );
sink.link_();
}
else
{
sink.text( ruleName );
}
List<String> attribnames = new ArrayList<>( Arrays.asList( checkerConfig.getAttributeNames() ) );
attribnames.remove( "severity" ); // special value (deserves unique column)
if ( !attribnames.isEmpty() )
{
sink.list();
for ( String name : attribnames )
{
sink.listItem();
sink.text( name );
String value = getConfigAttribute( checkerConfig, null, name, "" );
// special case, Header.header and RegexpHeader.header
if ( "header".equals( name ) && ( "Header".equals( ruleName ) || "RegexpHeader".equals( ruleName ) ) )
{
String[] lines = StringUtils.split( value, "\\n" );
int linenum = 1;
for ( String line : lines )
{
sink.lineBreak();
sink.rawText( "<span style=\"color: gray\">" );
sink.text( linenum + ":" );
sink.rawText( "</span>" );
sink.nonBreakingSpace();
sink.monospaced();
sink.text( line );
sink.monospaced_();
linenum++;
}
}
else if ( "headerFile".equals( name ) && "RegexpHeader".equals( ruleName ) )
{
sink.text( ": " );
sink.monospaced();
sink.text( "\"" );
if ( basedir != null )
{
// Make the headerFile value relative to ${basedir}
String path = siteTool.getRelativePath( value, basedir.getAbsolutePath() );
sink.text( path.replace( '\\', '/' ) );
}
else
{
sink.text( value );
}
sink.text( "\"" );
sink.monospaced_();
}
else
{
sink.text( ": " );
sink.monospaced();
sink.text( "\"" );
sink.text( value );
sink.text( "\"" );
sink.monospaced_();
}
sink.listItem_();
}
sink.list_();
}
sink.tableCell_();
// column 3: rule violation count
sink.tableCell();
sink.text( String.valueOf( ref.violations ) );
sink.tableCell_();
// column 4: severity
sink.tableCell();
// Grab the severity from the rule configuration, this time use error as default value
// Also pass along all parent configurations, so that we can try to find the severity there
String severity = getConfigAttribute( checkerConfig, parentConfiguration, "severity", "error" );
iconTool.iconSeverity( severity, IconTool.TEXT_SIMPLE );
sink.tableCell_();
sink.tableRow_();
}
/**
* Check if a violation matches a rule.
*
* @param event the violation to check
* @param ruleName The name of the rule
* @param expectedMessage A message that, if it's not null, will be matched to the message from the violation
* @param expectedSeverity A severity that, if it's not null, will be matched to the severity from the violation
* @return The number of rule violations
*/
public boolean matchRule( AuditEvent event, String ruleName, String expectedMessage, String expectedSeverity )
{
if ( !ruleName.equals( RuleUtil.getName( event ) ) )
{
return false;
}
// check message too, for those that have a specific one.
// like GenericIllegalRegexp and Regexp
if ( expectedMessage != null )
{
// event.getMessage() uses java.text.MessageFormat in its implementation.
// Read MessageFormat Javadoc about single quote:
// http://java.sun.com/j2se/1.4.2/docs/api/java/text/MessageFormat.html
String msgWithoutSingleQuote = StringUtils.replace( expectedMessage, "'", "" );
if ( ! ( expectedMessage.equals( event.getMessage() )
|| msgWithoutSingleQuote.equals( event.getMessage() ) ) )
{
return false;
}
}
// Check the severity. This helps to distinguish between
// different configurations for the same rule, where each
// configuration has a different severity, like JavadocMethod.
// See also https://issues.apache.org/jira/browse/MCHECKSTYLE-41
if ( expectedSeverity != null )
{
if ( !expectedSeverity.equals( event.getSeverityLevel().getName() ) )
{
return false;
}
}
return true;
}
private void doSeveritySummary( CheckstyleResults results )
{
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.checkstyle.summary" ) );
sink.sectionTitle1_();
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.files" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconTool.iconInfo( IconTool.TEXT_TITLE );
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconTool.iconWarning( IconTool.TEXT_TITLE );
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconTool.iconError( IconTool.TEXT_TITLE );
sink.tableHeaderCell_();
sink.tableRow_();
sink.tableRow();
sink.tableCell();
sink.text( String.valueOf( results.getFileCount() ) );
sink.tableCell_();
sink.tableCell();
sink.text( String.valueOf( results.getSeverityCount( SeverityLevel.INFO ) ) );
sink.tableCell_();
sink.tableCell();
sink.text( String.valueOf( results.getSeverityCount( SeverityLevel.WARNING ) ) );
sink.tableCell_();
sink.tableCell();
sink.text( String.valueOf( results.getSeverityCount( SeverityLevel.ERROR ) ) );
sink.tableCell_();
sink.tableRow_();
sink.table_();
sink.section1_();
}
private void doFilesSummary( CheckstyleResults results )
{
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.checkstyle.files" ) );
sink.sectionTitle1_();
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.file" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconTool.iconInfo( IconTool.TEXT_ABBREV );
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconTool.iconWarning( IconTool.TEXT_ABBREV );
sink.tableHeaderCell_();
sink.tableHeaderCell();
iconTool.iconError( IconTool.TEXT_ABBREV );
sink.tableHeaderCell_();
sink.tableRow_();
// Sort the files before writing them to the report
List<String> fileList = new ArrayList<>( results.getFiles().keySet() );
Collections.sort( fileList );
for ( String filename : fileList )
{
List<AuditEvent> violations = results.getFileViolations( filename );
if ( violations.isEmpty() )
{
// skip files without violations
continue;
}
sink.tableRow();
sink.tableCell();
sink.link( "#" + filename.replace( '/', '.' ) );
sink.text( filename );
sink.link_();
sink.tableCell_();
sink.tableCell();
sink.text( String.valueOf( results.getSeverityCount( violations, SeverityLevel.INFO ) ) );
sink.tableCell_();
sink.tableCell();
sink.text( String.valueOf( results.getSeverityCount( violations, SeverityLevel.WARNING ) ) );
sink.tableCell_();
sink.tableCell();
sink.text( String.valueOf( results.getSeverityCount( violations, SeverityLevel.ERROR ) ) );
sink.tableCell_();
sink.tableRow_();
}
sink.table_();
sink.section1_();
}
private void doDetails( CheckstyleResults results )
{
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.checkstyle.details" ) );
sink.sectionTitle1_();
// Sort the files before writing their details to the report
List<String> fileList = new ArrayList<>( results.getFiles().keySet() );
Collections.sort( fileList );
for ( String file : fileList )
{
List<AuditEvent> violations = results.getFileViolations( file );
if ( violations.isEmpty() )
{
// skip files without violations
continue;
}
sink.section2();
SinkEventAttributes attrs = new SinkEventAttributeSet();
attrs.addAttribute( SinkEventAttributes.ID, file.replace( '/', '.' ) );
sink.sectionTitle( Sink.SECTION_LEVEL_2, attrs );
sink.text( file );
sink.sectionTitle_( Sink.SECTION_LEVEL_2 );
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.column.severity" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.rule.category" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.rule" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.column.message" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.checkstyle.column.line" ) );
sink.tableHeaderCell_();
sink.tableRow_();
doFileEvents( violations, file );
sink.table_();
sink.section2_();
}
sink.section1_();
}
private void doFileEvents( List<AuditEvent> eventList, String filename )
{
for ( AuditEvent event : eventList )
{
SeverityLevel level = event.getSeverityLevel();
if ( ( getSeverityLevel() != null ) && !( getSeverityLevel() != level ) )
{
continue;
}
sink.tableRow();
sink.tableCell();
iconTool.iconSeverity( level.getName(), IconTool.TEXT_SIMPLE );
sink.tableCell_();
sink.tableCell();
String category = RuleUtil.getCategory( event );
if ( category != null )
{
sink.text( category );
}
sink.tableCell_();
sink.tableCell();
String ruleName = RuleUtil.getName( event );
if ( ruleName != null )
{
sink.text( ruleName );
}
sink.tableCell_();
sink.tableCell();
sink.text( event.getMessage() );
sink.tableCell_();
sink.tableCell();
int line = event.getLine();
if ( getXrefLocation() != null && line != 0 )
{
sink.link( getXrefLocation() + "/" + filename.replaceAll( "\\.java$", ".html" ) + "#L"
+ line );
sink.text( String.valueOf( line ) );
sink.link_();
}
else if ( line != 0 )
{
sink.text( String.valueOf( line ) );
}
sink.tableCell_();
sink.tableRow_();
}
}
public SeverityLevel getSeverityLevel()
{
return severityLevel;
}
public void setSeverityLevel( SeverityLevel severityLevel )
{
this.severityLevel = severityLevel;
}
public boolean isEnableRulesSummary()
{
return enableRulesSummary;
}
public void setEnableRulesSummary( boolean enableRulesSummary )
{
this.enableRulesSummary = enableRulesSummary;
}
public boolean isEnableSeveritySummary()
{
return enableSeveritySummary;
}
public void setEnableSeveritySummary( boolean enableSeveritySummary )
{
this.enableSeveritySummary = enableSeveritySummary;
}
public boolean isEnableFilesSummary()
{
return enableFilesSummary;
}
public void setEnableFilesSummary( boolean enableFilesSummary )
{
this.enableFilesSummary = enableFilesSummary;
}
public boolean isEnableRSS()
{
return enableRSS;
}
public void setEnableRSS( boolean enableRSS )
{
this.enableRSS = enableRSS;
}
public boolean isSkipEmptyViolationRules()
{
return skipEmptyViolationRules;
}
public void setSkipEmptyViolationRules( boolean showEmptyViolationRules )
{
this.skipEmptyViolationRules = showEmptyViolationRules;
}
public String getXrefLocation()
{
return xrefLocation;
}
public void setXrefLocation( String xrefLocation )
{
this.xrefLocation = xrefLocation;
}
public Configuration getCheckstyleConfig()
{
return checkstyleConfig;
}
public void setCheckstyleConfig( Configuration config )
{
this.checkstyleConfig = config;
}
public void setTreeWalkerNames( List<String> treeWalkerNames )
{
this.treeWalkerNames = treeWalkerNames;
}
public List<String> getTreeWalkerNames()
{
return treeWalkerNames;
}
/**
* Get the effective Checkstyle version at runtime.
* @return the MANIFEST implementation version of Checkstyle API package (can be <code>null</code>)
*/
private String getCheckstyleVersion()
{
Package checkstyleApiPackage = Configuration.class.getPackage();
return ( checkstyleApiPackage == null ) ? null : checkstyleApiPackage.getImplementationVersion();
}
public List<ConfReference> sortConfiguration( CheckstyleResults results )
{
List<ConfReference> result = new ArrayList<>();
sortConfiguration( result, checkstyleConfig, null, results );
Collections.sort( result );
return result;
}
private void sortConfiguration( List<ConfReference> result, Configuration config,
ChainedItem<Configuration> parent, CheckstyleResults results )
{
for ( Configuration childConfig : config.getChildren() )
{
String ruleName = childConfig.getName();
if ( treeWalkerNames.contains( ruleName ) )
{
// special sub-case: TreeWalker is the parent of multiple rules, not an effective rule
sortConfiguration( result, childConfig, new ChainedItem<>( config, parent ), results );
}
else
{
String fixedmessage = getConfigAttribute( childConfig, null, "message", null );
// Grab the severity from the rule configuration. Do not set default value here as
// it breaks our rule aggregate section entirely. The counts are off but this is
// not appropriate fix location per MCHECKSTYLE-365.
String configSeverity = getConfigAttribute( childConfig, null, "severity", null );
// count rule violations
long violations = 0;
for ( List<AuditEvent> errors : results.getFiles().values() )
{
for ( AuditEvent event : errors )
{
if ( matchRule( event, ruleName, fixedmessage, configSeverity ) )
{
violations++;
}
}
}
if ( violations > 0 || !skipEmptyViolationRules ) // forget rules without violations when not requested
{
String category = RuleUtil.getCategory( results.getModuleNameMap().get( ruleName ) );
if ( category != null )
{
result.add( new ConfReference( category, childConfig, parent, violations, result.size() ) );
}
}
}
}
}
private static class ConfReference
implements Comparable<ConfReference>
{
private final String category;
private final Configuration configuration;
private final ChainedItem<Configuration> parentConfiguration;
private final long violations;
private final int count;
ConfReference( String category, Configuration configuration,
ChainedItem<Configuration> parentConfiguration, long violations, int count )
{
this.category = category;
this.configuration = configuration;
this.parentConfiguration = parentConfiguration;
this.violations = violations;
this.count = count;
}
public int compareTo( ConfReference o )
{
int compare = category.compareTo( o.category );
if ( compare == 0 )
{
compare = configuration.getName().compareTo( o.configuration.getName() );
}
return ( compare == 0 ) ? ( o.count - count ) : compare;
}
}
private static class ChainedItem<T>
{
private final ChainedItem<T> parent;
private final T value;
ChainedItem( T value, ChainedItem<T> parent )
{
this.parent = parent;
this.value = value;
}
}
}