1919 */
2020package org .sonar .plugins .openedge .api ;
2121
22+ import java .io .IOException ;
2223import java .lang .annotation .Annotation ;
24+ import java .nio .charset .StandardCharsets ;
2325import java .util .List ;
26+ import java .util .Locale ;
2427import java .util .Set ;
28+ import java .util .regex .Matcher ;
29+ import java .util .regex .Pattern ;
30+
31+ import javax .annotation .Nullable ;
2532
2633import org .slf4j .Logger ;
2734import org .slf4j .LoggerFactory ;
2835import org .sonar .api .SonarRuntime ;
2936import org .sonar .api .rules .RuleType ;
37+ import org .sonar .api .server .rule .Context ;
38+ import org .sonar .api .server .rule .RuleDescriptionSection ;
39+ import org .sonar .api .server .rule .RuleDescriptionSectionBuilder ;
3040import org .sonar .api .server .rule .RulesDefinition ;
3141import org .sonar .api .server .rule .RulesDefinition .NewRepository ;
3242import org .sonar .api .server .rule .RulesDefinition .NewRule ;
5565import com .google .common .collect .Lists ;
5666import com .google .common .collect .Sets ;
5767
68+ import static org .sonar .api .server .rule .RuleDescriptionSection .RuleDescriptionSectionKeys .HOW_TO_FIX_SECTION_KEY ;
69+ import static org .sonar .api .server .rule .RuleDescriptionSection .RuleDescriptionSectionKeys .INTRODUCTION_SECTION_KEY ;
70+ import static org .sonar .api .server .rule .RuleDescriptionSection .RuleDescriptionSectionKeys .RESOURCES_SECTION_KEY ;
71+ import static org .sonar .api .server .rule .RuleDescriptionSection .RuleDescriptionSectionKeys .ROOT_CAUSE_SECTION_KEY ;
72+
5873/**
5974 * Utility class which helps setting up an implementation of {@link RulesDefinition} with a list of rule classes
6075 * annotated with {@link Rule}, {@link RuleProperty} and one of:
7489public class AnnotationBasedRulesDefinition {
7590 private static final Logger LOGGER = LoggerFactory .getLogger (AnnotationBasedRulesDefinition .class );
7691
92+ private static final String CODE_EXAMPLES_HEADER = "<h3>Code examples</h3>" ;
93+ private static final String WHY_SECTION_HEADER = "<h2>Why is this an issue\\ ?</h2>" ;
94+ private static final String HOW_TO_FIX_SECTION_HEADER = "<h2>How to fix it</h2>" ;
95+ private static final String RESOURCES_SECTION_HEADER = "<h2>Resources</h2>" ;
96+ private static final String HOW_TO_FIX_FRAMEWORK_SECTION_REGEX = "<h2>How to fix it in (?:(?:an|a|the)\\ s)?(?<displayName>.*)</h2>" ;
97+ private static final Pattern HOW_TO_FIX_SECTION_PATTERN = Pattern .compile (HOW_TO_FIX_SECTION_HEADER );
98+ private static final Pattern HOW_TO_FIX_FRAMEWORK_SECTION_PATTERN = Pattern .compile (HOW_TO_FIX_FRAMEWORK_SECTION_REGEX );
99+
77100 private final NewRepository repository ;
78- private final ExternalDescriptionLoader externalDescriptionLoader ;
101+ private final String basePath ;
79102 private final SonarRuntime runtime ;
80103
81104 public AnnotationBasedRulesDefinition (NewRepository repository , String languageKey , SonarRuntime runtime ) {
82105 this .repository = repository ;
83- String externalDescriptionBasePath = String .format ("/rules/%s/%s/" , languageKey , repository .key ());
84- this .externalDescriptionLoader = new ExternalDescriptionLoader (externalDescriptionBasePath );
106+ this .basePath = String .format ("/rules/%s/%s/" , languageKey , repository .key ());
85107 this .runtime = runtime ;
86108 }
87109
@@ -97,7 +119,7 @@ public void addRuleClasses(boolean failIfNoExplicitKey, Iterable<Class> ruleClas
97119 for (Class <?> ruleClass : ruleClasses ) {
98120 NewRule rule = newRule (ruleClass , failIfNoExplicitKey );
99121 rule .setTemplate (AnnotationUtils .getAnnotation (ruleClass , RuleTemplate .class ) != null );
100- externalDescriptionLoader . addHtmlDescription (rule , ruleClass );
122+ setupDocumentation (rule , ruleClass );
101123 setupSecurityModel (rule , ruleClass );
102124 if (runtime .getApiVersion ().isGreaterThanOrEqual (Version .create (10 , 1 )))
103125 setupCleanCode (rule , ruleClass );
@@ -135,8 +157,6 @@ private void setupSecurityModel(NewRule rule, Class<?> ruleClass) {
135157 var hotspotAnnotation = AnnotationUtils .getAnnotation (ruleClass , SecurityHotspot .class );
136158 if (hotspotAnnotation != null ) {
137159 rule .setType (RuleType .SECURITY_HOTSPOT );
138- setOwasp (rule , OwaspTop10Version .Y2017 , hotspotAnnotation .owasp ());
139- setCwe (rule , hotspotAnnotation .cwe ());
140160 }
141161 var cweAnnotation = AnnotationUtils .getAnnotation (ruleClass , CWE .class );
142162 if (cweAnnotation != null )
@@ -208,23 +228,96 @@ private void setupSqaleModel(NewRule rule, Class<?> ruleClass) {
208228 }
209229 }
210230
211- private class ExternalDescriptionLoader {
212- private final String resourceBasePath ;
231+ private void setupDocumentation (NewRule rule , Class <?> clz ) {
232+ var url01 = clz .getResource (basePath + rule .key ().replace ('.' , '/' ) + ".html" );
233+ var url02 = clz .getResource (basePath + rule .key ().replace ('.' , '/' ) + ".sections.html" );
234+ if (url02 != null ) {
235+ try (var in = url02 .openStream ()) {
236+ var desc = new String (in .readAllBytes (), StandardCharsets .UTF_8 );
237+ rule .setHtmlDescription (desc ); // Compatibility with old versions of SonarQube
238+ setupEducationDocumentation (rule , desc );
239+ } catch (IOException caught ) {
240+ rule .setHtmlDescription ("<p>Invalid description</p>" );
241+ }
242+ } else if (url01 != null ) {
243+ rule .setHtmlDescription (url01 );
244+ } else {
245+ rule .setHtmlDescription ("<p>No description</p>" );
246+ LOGGER .warn ("No HTML description found for rule {}" , rule .key ());
247+ }
248+ }
249+
250+ // Adapted from org.sonarsource.analyzer.commons.EducationRuleLoader
251+ private void setupEducationDocumentation (NewRule rule , String description ) {
252+ // The "Why is this an issue?" section is expected.
253+ var split = description .split (WHY_SECTION_HEADER );
254+
255+ // Adding the introduction section if not empty.
256+ addSection (rule , INTRODUCTION_SECTION_KEY , split [0 ]);
257+ split = split [1 ].split (RESOURCES_SECTION_HEADER );
213258
214- public ExternalDescriptionLoader (String resourceBasePath ) {
215- this .resourceBasePath = resourceBasePath ;
259+ // Filtering out the "<h3>Code examples</h3>" title.
260+ var rootCauseAndHowToFixItSections = split [0 ].replace (CODE_EXAMPLES_HEADER , "" );
261+
262+ // Either the generic "How to fix it" section or at least one framework specific "How to fix it in <framework_name>"
263+ // section is expected.
264+ var frameworkSpecificHowToFixItSectionMatcher = HOW_TO_FIX_FRAMEWORK_SECTION_PATTERN .matcher (
265+ rootCauseAndHowToFixItSections );
266+ var hasFrameworkSpecificHowToFixItSection = frameworkSpecificHowToFixItSectionMatcher .find ();
267+ var hasGenericHowToFixItSection = HOW_TO_FIX_SECTION_PATTERN .matcher (rootCauseAndHowToFixItSections ).find ();
268+ if (hasGenericHowToFixItSection && hasFrameworkSpecificHowToFixItSection ) {
269+ throw new IllegalStateException (String .format (
270+ "Invalid education rule format for '%s', rule description has both generic and framework-specific 'How to fix it' sections" ,
271+ rule .key ()));
272+ } else if (hasFrameworkSpecificHowToFixItSection ) {
273+ // Splitting by the "How to fix in <displayName>" will return an array where each element after the first is the
274+ // content related to a given framework.
275+ var innerSplit = rootCauseAndHowToFixItSections .split (HOW_TO_FIX_FRAMEWORK_SECTION_REGEX );
276+ addSection (rule , ROOT_CAUSE_SECTION_KEY , innerSplit [0 ]);
277+ addContextSpecificHowToFixItSection (rule , innerSplit , frameworkSpecificHowToFixItSectionMatcher );
278+ } else if (hasGenericHowToFixItSection ) {
279+ // Rule has the generic "How to fix it" section.
280+ var innerSplit = rootCauseAndHowToFixItSections .split (HOW_TO_FIX_SECTION_HEADER );
281+ addSection (rule , ROOT_CAUSE_SECTION_KEY , innerSplit [0 ]);
282+ addSection (rule , HOW_TO_FIX_SECTION_KEY , innerSplit [1 ]);
283+ } else {
284+ // No "How to fix it" section for the rule, the only section present is "Why is it an issue".
285+ addSection (rule , ROOT_CAUSE_SECTION_KEY , rootCauseAndHowToFixItSections );
216286 }
217287
218- public void addHtmlDescription (NewRule rule , Class <?> clz ) {
219- var path = resourceBasePath + rule .key ().replace ('.' , '/' ) + ".html" ;
220- var url = clz .getResource (path );
221- if (url != null ) {
222- rule .setHtmlDescription (url );
223- } else {
224- rule .setHtmlDescription ("<p>No description</p>" );
225- LOGGER .warn ("No HTML description found in path {} for rule {}" , path , rule .key ());
226- }
288+ // "Resources" section is optional.
289+ if (split .length > 1 ) {
290+ addSection (rule , RESOURCES_SECTION_KEY , split [1 ]);
227291 }
228292 }
229293
294+ private static void addContextSpecificHowToFixItSection (NewRule rule , String [] split , Matcher m ) {
295+ var match = true ;
296+ var splitIndex = 1 ;
297+ while (match ) {
298+ var displayName = m .group ("displayName" ).trim ();
299+ var contextSpecificContent = split [splitIndex ];
300+ var key = displayName .toLowerCase (Locale .ROOT ).replaceAll ("[^a-z0-9]" , "_" );
301+ addSection (rule , HOW_TO_FIX_SECTION_KEY , contextSpecificContent , new Context (key , displayName ));
302+ match = m .find ();
303+ splitIndex ++;
304+ }
305+ }
306+
307+ private static void addSection (NewRule rule , String sectionKey , String content ) {
308+ addSection (rule , sectionKey , content , null );
309+ }
310+
311+ private static void addSection (NewRule rule , String sectionKey , String content , @ Nullable Context context ) {
312+ if (content .isBlank ())
313+ return ;
314+
315+ RuleDescriptionSectionBuilder sectionBuilder = RuleDescriptionSection .builder () //
316+ .sectionKey (sectionKey ) //
317+ .htmlContent (content .trim ()) //
318+ .context (context );
319+
320+ rule .addDescriptionSection (sectionBuilder .build ());
321+ }
322+
230323}
0 commit comments