]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16614 Display the most relevant rule description context for an issue
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Wed, 6 Jul 2022 13:05:04 +0000 (15:05 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 8 Jul 2022 20:02:48 +0000 (20:02 +0000)
13 files changed:
server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
server/sonar-web/src/main/js/apps/coding-rules/rule.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
server/sonar-web/src/main/js/types/issues.ts
server/sonar-web/src/main/js/types/types.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 1c64cb8aca2863905201409ce855f213abda50d8..f6e2563257aa6785565f29456e7a48d6a52c35a0 100644 (file)
@@ -134,12 +134,12 @@ export default class CodingRulesMock {
           {
             key: RuleDescriptionSections.HOW_TO_FIX,
             content: 'This how to fix for spring',
-            context: { displayName: 'Spring' }
+            context: { key: 'spring', displayName: 'Spring' }
           },
           {
             key: RuleDescriptionSections.HOW_TO_FIX,
             content: 'This how to fix for spring boot',
-            context: { displayName: 'Spring boot' }
+            context: { key: 'spring_boot', displayName: 'Spring boot' }
           },
           {
             key: RuleDescriptionSections.RESOURCES,
index 0957a3b5f80d30175613dc907c9336a7e731f463..054ca7cde59d61c98d0a01b11ef54d38539b68f4 100644 (file)
@@ -146,7 +146,8 @@ export default class IssuesServiceMock {
             endLine: 25,
             startOffset: 0,
             endOffset: 1
-          }
+          },
+          ruleDescriptionContextKey: 'spring'
         }),
         snippets: keyBy(
           [
@@ -253,6 +254,7 @@ export default class IssuesServiceMock {
               content: '<p> Context 1 content<p>',
               key: RuleDescriptionSections.HOW_TO_FIX,
               context: {
+                key: 'spring',
                 displayName: 'Spring'
               }
             },
@@ -260,6 +262,7 @@ export default class IssuesServiceMock {
               content: '<p> Context 2 content<p>',
               key: RuleDescriptionSections.HOW_TO_FIX,
               context: {
+                key: 'context_2',
                 displayName: 'Context 2'
               }
             },
@@ -267,6 +270,7 @@ export default class IssuesServiceMock {
               content: '<p> Context 3 content<p>',
               key: RuleDescriptionSections.HOW_TO_FIX,
               context: {
+                key: 'context_3',
                 displayName: 'Context 3'
               }
             },
index 3c3e647ef73c76bd07b7facebb3fe0747319fdd0..3422bce50d53584796867a5ddb8f053d6cd6d92e 100644 (file)
@@ -69,7 +69,10 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {
 
   computeState() {
     const { ruleDetails } = this.props;
-    const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key');
+    const descriptionSectionsByKey = groupBy(
+      ruleDetails.descriptionSections,
+      section => section.key
+    );
 
     const tabs = [
       {
@@ -78,34 +81,38 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> {
           ruleDetails.type === 'SECURITY_HOTSPOT'
             ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
             : translate('coding_rules.description_section.title.root_cause'),
-        content: groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE] && (
-          <RuleDescription description={groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]} />
+        content: descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE] && (
+          <RuleDescription
+            sections={descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]}
+          />
         )
       },
       {
         key: RuleTabKeys.AssessTheIssue,
         label: translate('coding_rules.description_section.title', RuleTabKeys.AssessTheIssue),
-        content: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+        content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
           <RuleDescription
-            description={groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
           />
         )
       },
       {
         key: RuleTabKeys.HowToFixIt,
         label: translate('coding_rules.description_section.title', RuleTabKeys.HowToFixIt),
-        content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && (
-          <RuleDescription description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} />
+        content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+          <RuleDescription
+            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+          />
         )
       },
       {
         key: RuleTabKeys.MoreInfo,
         label: translate('coding_rules.description_section.title', RuleTabKeys.MoreInfo),
         content: (ruleDetails.genericConcepts ||
-          groupedDescriptions[RuleDescriptionSections.RESOURCES]) && (
+          descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
           <MoreInfoRuleDescription
             genericConcepts={ruleDetails.genericConcepts}
-            description={groupedDescriptions[RuleDescriptionSections.RESOURCES]}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
           />
         )
       }
index a6d2310a637262c304ea7f2ce6c899ee676790ad..c0482e6d22e06db41d2bd31e77978ae832144013 100644 (file)
@@ -27,6 +27,7 @@ export enum RuleDescriptionSections {
 }
 
 export interface RuleDescriptionContext {
+  key: string;
   displayName: string;
 }
 
index 36f41a9600f1f311339ffa5dba239d36b4c55794..40e6a7092fb73b98ebf023b2aca8765d48a3ac8d 100644 (file)
@@ -47,51 +47,70 @@ it('should show generic concpet', async () => {
 
 it('should open issue and navigate', async () => {
   const user = userEvent.setup();
+
   renderIssueApp();
+
+  // Select an issue with an advanced rule
   expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument();
   await user.click(screen.getByRole('region', { name: 'Fix that' }));
+
+  // Are rule headers present?
   expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument();
 
-  expect(screen.getByRole('button', { name: `issue.tabs.more_info` })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: `issue.tabs.more_info` }));
-  expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
+  // Select the "why is this an issue" tab and check its content
+  expect(screen.getByRole('button', { name: `issue.tabs.why` })).toBeInTheDocument();
+  await user.click(screen.getByRole('button', { name: `issue.tabs.why` }));
+  expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
 
+  // Select the "how to fix it" tab
   expect(screen.getByRole('button', { name: `issue.tabs.how` })).toBeInTheDocument();
   await user.click(screen.getByRole('button', { name: `issue.tabs.how` }));
+
+  // Is the context selector present with the expected values and default selection?
   expect(screen.getByRole('radio', { name: 'Context 2' })).toBeInTheDocument();
   expect(screen.getByRole('radio', { name: 'Context 3' })).toBeInTheDocument();
   expect(screen.getByRole('radio', { name: 'Spring' })).toBeInTheDocument();
   expect(
     screen.getByRole('radio', { name: 'coding_rules.description_context_other' })
   ).toBeInTheDocument();
+  expect(screen.getByRole('radio', { name: 'Spring' })).toBeChecked();
 
+  // Select context 2 and check tab content
   await user.click(screen.getByRole('radio', { name: 'Context 2' }));
   expect(screen.getByText('Context 2 content')).toBeInTheDocument();
 
+  // Select the "other" context and check tab content
   await user.click(screen.getByRole('radio', { name: 'coding_rules.description_context_other' }));
   expect(screen.getByText('coding_rules.context.others.title')).toBeInTheDocument();
   expect(screen.getByText('coding_rules.context.others.description.first')).toBeInTheDocument();
   expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument();
 
-  expect(screen.getByRole('button', { name: `issue.tabs.why` })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: `issue.tabs.why` }));
-  expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
+  // Select the resources tab and check its content
+  expect(screen.getByRole('button', { name: `issue.tabs.more_info` })).toBeInTheDocument();
+  await user.click(screen.getByRole('button', { name: `issue.tabs.more_info` }));
+  expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
 
+  // Select the previous issue (with a simple rule) through keyboard shortcut
   await user.keyboard('{ArrowUp}');
 
+  // Are rule headers present?
   expect(screen.getByRole('heading', { level: 1, name: 'Fix this' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
 
+  // Select the "why is this an issue tab" and check its content
   expect(screen.getByRole('button', { name: `issue.tabs.why` })).toBeInTheDocument();
   await user.click(screen.getByRole('button', { name: `issue.tabs.why` }));
   expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument();
 
+  // Select the previous issue (with a simple rule) through keyboard shortcut
   await user.keyboard('{ArrowUp}');
 
+  // Are rule headers present?
   expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument();
   expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
 
+  // Select the "Where is the issue" tab and check its content
   expect(screen.getByRole('button', { name: `issue.tabs.code` })).toBeInTheDocument();
   await user.click(screen.getByRole('button', { name: `issue.tabs.code` }));
   expect(screen.getByRole('region', { name: 'Issue on file' })).toBeInTheDocument();
index 4821f37896ce2b0ef4a8d79354eaca49ca2cbee2..cfd70022e55b62a1e618e8dbf483fde3937bf47c 100644 (file)
@@ -82,17 +82,24 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
   };
 
   computeTabs() {
-    const { ruleDetails, codeTabContent } = this.props;
-    const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key');
+    const {
+      ruleDetails,
+      codeTabContent,
+      issue: { ruleDescriptionContextKey }
+    } = this.props;
+    const descriptionSectionsByKey = groupBy(
+      ruleDetails.descriptionSections,
+      section => section.key
+    );
 
     if (ruleDetails.htmlNote) {
-      if (groupedDescriptions[RuleDescriptionSections.RESOURCES] !== undefined) {
+      if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] !== undefined) {
         // We add the extended description (htmlNote) in the first context, in case there are contexts
         // Extended description will get reworked in future
-        groupedDescriptions[RuleDescriptionSections.RESOURCES][0].content +=
+        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
           '<br/>' + ruleDetails.htmlNote;
       } else {
-        groupedDescriptions[RuleDescriptionSections.RESOURCES] = [
+        descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
           {
             key: RuleDescriptionSections.RESOURCES,
             content: ruleDetails.htmlNote
@@ -101,9 +108,9 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
       }
     }
 
-    const rootCause =
-      groupedDescriptions[RuleDescriptionSections.DEFAULT] ||
-      groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE];
+    const rootCauseDescriptionSections =
+      descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+      descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
 
     return [
       {
@@ -114,28 +121,32 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> {
       {
         key: IssueTabKeys.WhyIsThisAnIssue,
         label: translate('issue.tabs', IssueTabKeys.WhyIsThisAnIssue),
-        content: rootCause && (
+        content: rootCauseDescriptionSections && (
           <RuleDescription
-            description={rootCause}
-            isDefault={groupedDescriptions[RuleDescriptionSections.DEFAULT] !== undefined}
+            sections={rootCauseDescriptionSections}
+            isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
+            defaultContextKey={ruleDescriptionContextKey}
           />
         )
       },
       {
         key: IssueTabKeys.HowToFixIt,
         label: translate('issue.tabs', IssueTabKeys.HowToFixIt),
-        content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && (
-          <RuleDescription description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} />
+        content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+          <RuleDescription
+            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+            defaultContextKey={ruleDescriptionContextKey}
+          />
         )
       },
       {
         key: IssueTabKeys.MoreInfo,
         label: translate('issue.tabs', IssueTabKeys.MoreInfo),
         content: (ruleDetails.genericConcepts ||
-          groupedDescriptions[RuleDescriptionSections.RESOURCES]) && (
+          descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
           <MoreInfoRuleDescription
             genericConcepts={ruleDetails.genericConcepts}
-            description={groupedDescriptions[RuleDescriptionSections.RESOURCES]}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
           />
         )
       }
index 7e53f144e69251df53b4ca6be566636cc1dc34c4..e57ac14735762c727883540ad2fcdc27137ddf87 100644 (file)
@@ -120,10 +120,10 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
 
   computeTabs() {
     const { ruleDescriptionSections, codeTabContent } = this.props;
-    const groupedDescriptions = groupBy(ruleDescriptionSections, description => description.key);
-    const rootCause =
-      groupedDescriptions[RuleDescriptionSections.DEFAULT] ||
-      groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE];
+    const descriptionSectionsByKey = groupBy(ruleDescriptionSections, section => section.key);
+    const rootCauseDescriptionSections =
+      descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+      descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE];
 
     return [
       {
@@ -134,14 +134,16 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
       {
         key: TabKeys.RiskDescription,
         label: translate('hotspots.tabs.risk_description'),
-        content: rootCause && <RuleDescription description={rootCause} isDefault={true} />
+        content: rootCauseDescriptionSections && (
+          <RuleDescription sections={rootCauseDescriptionSections} isDefault={true} />
+        )
       },
       {
         key: TabKeys.VulnerabilityDescription,
         label: translate('hotspots.tabs.vulnerability_description'),
-        content: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+        content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
           <RuleDescription
-            description={groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
             isDefault={true}
           />
         )
@@ -149,9 +151,9 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State>
       {
         key: TabKeys.FixRecommendation,
         label: translate('hotspots.tabs.fix_recommendations'),
-        content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && (
+        content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
           <RuleDescription
-            description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]}
+            sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
             isDefault={true}
           />
         )
index ab90bf8c2bab7cccf5929b89ad140a8e4a283b79..844b9f05c07dbd130646ccebc448a9922f1abe11 100644 (file)
@@ -20,7 +20,8 @@ exports[`should render correctly: fix 1`] = `
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "cause",
@@ -28,14 +29,14 @@ exports[`should render correctly: fix 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "risk",
           "label": "hotspots.tabs.risk_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "assess",
@@ -43,14 +44,14 @@ exports[`should render correctly: fix 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "vulnerability",
           "label": "hotspots.tabs.vulnerability_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "how",
@@ -58,7 +59,6 @@ exports[`should render correctly: fix 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "fix",
           "label": "hotspots.tabs.fix_recommendations",
@@ -70,7 +70,8 @@ exports[`should render correctly: fix 1`] = `
     className="bordered huge-spacer-bottom"
   >
     <RuleDescription
-      description={
+      isDefault={true}
+      sections={
         Array [
           Object {
             "content": "how",
@@ -78,7 +79,6 @@ exports[`should render correctly: fix 1`] = `
           },
         ]
       }
-      isDefault={true}
     />
   </div>
 </Fragment>
@@ -104,7 +104,8 @@ exports[`should render correctly: risk 1`] = `
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "cause",
@@ -112,14 +113,14 @@ exports[`should render correctly: risk 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "risk",
           "label": "hotspots.tabs.risk_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "assess",
@@ -127,14 +128,14 @@ exports[`should render correctly: risk 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "vulnerability",
           "label": "hotspots.tabs.vulnerability_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "how",
@@ -142,7 +143,6 @@ exports[`should render correctly: risk 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "fix",
           "label": "hotspots.tabs.fix_recommendations",
@@ -184,7 +184,8 @@ exports[`should render correctly: vulnerability 1`] = `
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "cause",
@@ -192,14 +193,14 @@ exports[`should render correctly: vulnerability 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "risk",
           "label": "hotspots.tabs.risk_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "assess",
@@ -207,14 +208,14 @@ exports[`should render correctly: vulnerability 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "vulnerability",
           "label": "hotspots.tabs.vulnerability_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "how",
@@ -222,7 +223,6 @@ exports[`should render correctly: vulnerability 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "fix",
           "label": "hotspots.tabs.fix_recommendations",
@@ -234,7 +234,8 @@ exports[`should render correctly: vulnerability 1`] = `
     className="bordered huge-spacer-bottom"
   >
     <RuleDescription
-      description={
+      isDefault={true}
+      sections={
         Array [
           Object {
             "content": "assess",
@@ -242,7 +243,6 @@ exports[`should render correctly: vulnerability 1`] = `
           },
         ]
       }
-      isDefault={true}
     />
   </div>
 </Fragment>
@@ -268,7 +268,8 @@ exports[`should render correctly: with comments or changelog element 1`] = `
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "cause",
@@ -276,14 +277,14 @@ exports[`should render correctly: with comments or changelog element 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "risk",
           "label": "hotspots.tabs.risk_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "assess",
@@ -291,14 +292,14 @@ exports[`should render correctly: with comments or changelog element 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "vulnerability",
           "label": "hotspots.tabs.vulnerability_description",
         },
         Object {
           "content": <RuleDescription
-            description={
+            isDefault={true}
+            sections={
               Array [
                 Object {
                   "content": "how",
@@ -306,7 +307,6 @@ exports[`should render correctly: with comments or changelog element 1`] = `
                 },
               ]
             }
-            isDefault={true}
           />,
           "key": "fix",
           "label": "hotspots.tabs.fix_recommendations",
index bafca86796fb7d9b7a122d0a389c5eacae2bb62f..7e51817a4f482cad71660bcfc2b23a211db001a5 100644 (file)
@@ -27,7 +27,7 @@ import RuleDescription from './RuleDescription';
 import './style.css';
 
 interface Props {
-  description?: RuleDescriptionSection[];
+  sections?: RuleDescriptionSection[];
   genericConcepts?: string[];
 }
 
@@ -36,17 +36,17 @@ const GENERIC_CONCPET_MAP: Dict<React.ComponentType> = {
   least_trust_principle: LeastTrustPrinciple
 };
 
-export default function MoreInfoRuleDescription({ description = [], genericConcepts = [] }: Props) {
+export default function MoreInfoRuleDescription({ sections = [], genericConcepts = [] }: Props) {
   return (
     <>
-      {description.length > 0 && (
+      {sections.length > 0 && (
         <>
           <div className="big-padded-left big-padded-right big-padded-top rule-desc">
             <h2 className="null-spacer-bottom">
               {translate('coding_rules.more_info.resources.title')}
             </h2>
           </div>
-          <RuleDescription key="more-info" description={description} />
+          <RuleDescription key="more-info" sections={sections} />
         </>
       )}
 
index 8935766ded8a30fa7e1fc119497d0dd6b9d5b6b2..fcf0b3784c0e09e27e021079ca1993bffd9322ec 100644 (file)
 import classNames from 'classnames';
 import * as React from 'react';
 import { RuleDescriptionSection } from '../../apps/coding-rules/rule';
-import { translate } from '../../helpers/l10n';
+import { translate, translateWithParameters } from '../../helpers/l10n';
 import { sanitizeString } from '../../helpers/sanitize';
 import RadioToggle from '../controls/RadioToggle';
+import { Alert } from '../ui/Alert';
 import OtherContextOption from './OtherContextOption';
 
 const OTHERS_KEY = 'others';
 
 interface Props {
   isDefault?: boolean;
-  description: RuleDescriptionSection[];
+  sections: RuleDescriptionSection[];
+  defaultContextKey?: string;
 }
 
 interface State {
   contexts: RuleDescriptionContextDisplay[];
+  defaultContext?: RuleDescriptionContextDisplay;
   selectedContext?: RuleDescriptionContextDisplay;
 }
 
@@ -46,23 +49,32 @@ interface RuleDescriptionContextDisplay {
 export default class RuleDescription extends React.PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
-    this.state = this.computeState(props.description);
+    this.state = this.computeState();
   }
 
   componentDidUpdate(prevProps: Props) {
-    if (prevProps.description !== this.props.description) {
-      this.setState(this.computeState(this.props.description));
+    const { sections, defaultContextKey } = this.props;
+
+    if (prevProps.sections !== sections || prevProps.defaultContextKey !== defaultContextKey) {
+      this.setState(this.computeState());
     }
   }
 
-  computeState = (descriptions: RuleDescriptionSection[]) => {
-    const contexts = descriptions
-      .map(sec => ({
-        displayName: sec.context?.displayName || '',
-        content: sec.content,
-        key: sec.key.toString()
+  computeState = () => {
+    const { sections, defaultContextKey } = this.props;
+
+    const contexts = sections
+      .filter(
+        (
+          section
+        ): section is RuleDescriptionSection & Required<Pick<RuleDescriptionSection, 'context'>> =>
+          section.context != null
+      )
+      .map(section => ({
+        displayName: section.context.displayName || section.context.key,
+        content: section.content,
+        key: section.context.key
       }))
-      .filter(sec => sec.displayName !== '')
       .sort((a, b) => a.displayName.localeCompare(b.displayName));
 
     if (contexts.length > 0) {
@@ -73,9 +85,16 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
       });
     }
 
+    let defaultContext: RuleDescriptionContextDisplay | undefined;
+
+    if (defaultContextKey) {
+      defaultContext = contexts.find(context => context.key === defaultContextKey);
+    }
+
     return {
       contexts,
-      selectedContext: contexts[0]
+      defaultContext,
+      selectedContext: defaultContext ?? contexts[0]
     };
   };
 
@@ -89,59 +108,66 @@ export default class RuleDescription extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { description, isDefault } = this.props;
-    const { contexts } = this.state;
-    const { selectedContext } = this.state;
+    const { sections, isDefault } = this.props;
+    const { contexts, defaultContext, selectedContext } = this.state;
 
     const options = contexts.map(ctxt => ({
       label: ctxt.displayName,
       value: ctxt.displayName
     }));
 
-    if (!description[0].context && description.length === 1) {
+    if (contexts.length > 0 && selectedContext) {
       return (
         <div
           className={classNames('big-padded', {
             markdown: isDefault,
             'rule-desc': !isDefault
-          })}
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{
-            __html: sanitizeString(description[0].content)
-          }}
-        />
+          })}>
+          <div className="rules-context-description">
+            <h2 className="rule-contexts-title">
+              {translate('coding_rules.description_context_title')}
+            </h2>
+            {defaultContext && (
+              <Alert variant="info" display="inline" className="big-spacer-bottom">
+                {translateWithParameters(
+                  'coding_rules.description_context_default_information',
+                  defaultContext.displayName
+                )}
+              </Alert>
+            )}
+            <div>
+              <RadioToggle
+                className="big-spacer-bottom"
+                name="filter"
+                onCheck={this.handleToggleContext}
+                options={options}
+                value={selectedContext.displayName}
+              />
+            </div>
+            {selectedContext.key === OTHERS_KEY ? (
+              <OtherContextOption />
+            ) : (
+              <div
+                /* eslint-disable-next-line react/no-danger */
+                dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }}
+              />
+            )}
+          </div>
+        </div>
       );
     }
-    if (!selectedContext) {
-      return null;
-    }
+
     return (
       <div
         className={classNames('big-padded', {
           markdown: isDefault,
           'rule-desc': !isDefault
-        })}>
-        <div className="rules-context-description">
-          <h2 className="rule-contexts-title">
-            {translate('coding_rules.description_context_title')}
-          </h2>
-          <RadioToggle
-            className="big-spacer-bottom"
-            name="filter"
-            onCheck={this.handleToggleContext}
-            options={options}
-            value={selectedContext.displayName}
-          />
-          {selectedContext.key === OTHERS_KEY ? (
-            <OtherContextOption />
-          ) : (
-            <div
-              /* eslint-disable-next-line react/no-danger */
-              dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }}
-            />
-          )}
-        </div>
-      </div>
+        })}
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{
+          __html: sanitizeString(sections[0].content)
+        }}
+      />
     );
   }
 }
index ea82a595d9fff84a2bb7663a95c984063d5acb97..eca49843e045985ca21b94c32ba7354044624aec 100644 (file)
@@ -60,6 +60,7 @@ export interface RawIssue {
   status: string;
   textRange?: TextRange;
   type: IssueType;
+  ruleDescriptionContextKey?: string;
 }
 
 export interface IssueResponse {
index 64d9b22c78879fe0d94b2fae341dc812b8484127..ba2717c72de25fc7ce38087911a341b850df41c5 100644 (file)
@@ -285,6 +285,7 @@ export interface Issue {
   pullRequest?: string;
   resolution?: string;
   rule: string;
+  ruleDescriptionContextKey?: string;
   ruleName: string;
   ruleStatus?: string;
   secondaryLocations: FlowLocation[];
index 2815bb88e1c391f324dfbe6589c302be68c2dd09..add27b64310813cba81cc0f2ff11afe4690a172b 100644 (file)
@@ -1913,6 +1913,7 @@ coding_rules.description_section.title.how_to_fix=How to fix it?
 coding_rules.description_section.title.more_info=More Info
 
 coding_rules.description_context_title=Which component or framework contains the issue?
+coding_rules.description_context_default_information={0} was detected as the most relevant component or framework for this issue.
 coding_rules.description_context_other=Other
 
 coding_rules.more_info.generic_concept.title=Security principles