]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14120 Display file-level issues with multi-locations
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 30 Nov 2020 17:03:18 +0000 (18:03 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 3 Dec 2020 20:06:38 +0000 (20:06 +0000)
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts

index 704fe4fdde351e78c47183c21fffe40b9ef98771..5a0d3207965c620a49d3b376a0400c10ad50f405 100644 (file)
@@ -797,7 +797,8 @@ export default class App extends React.PureComponent<Props, State> {
   handleIssueChange = (issue: T.Issue) => {
     this.refreshBranchStatus();
     this.setState(state => ({
-      issues: state.issues.map(candidate => (candidate.key === issue.key ? issue : candidate))
+      issues: state.issues.map(candidate => (candidate.key === issue.key ? issue : candidate)),
+      openIssue: state.openIssue && state.openIssue.key === issue.key ? issue : state.openIssue
     }));
   };
 
index 5cb0f444f46f6d0a734a0528f4ba04e1c2d871f2..7f639106c24efb79882dc59c1857d18dbd669657 100644 (file)
@@ -353,7 +353,7 @@ it('should refresh branch status if issues are updated', async () => {
 
   const updatedIssue: T.Issue = { ...ISSUES[0], type: 'SECURITY_HOTSPOT' };
   instance.handleIssueChange(updatedIssue);
-  expect(wrapper.state('issues')).toEqual([updatedIssue, ISSUES[1], ISSUES[2], ISSUES[3]]);
+  expect(wrapper.state().issues).toEqual([updatedIssue, ISSUES[1], ISSUES[2], ISSUES[3]]);
   expect(fetchBranchStatus).toBeCalledWith(branchLike, component.key);
 
   fetchBranchStatus.mockClear();
@@ -365,6 +365,18 @@ it('should refresh branch status if issues are updated', async () => {
   expect(fetchBranchStatus).toBeCalled();
 });
 
+it('should update the open issue when it is changed', async () => {
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+
+  wrapper.setState({ openIssue: ISSUES[0] });
+
+  const updatedIssue: T.Issue = { ...ISSUES[0], type: 'SECURITY_HOTSPOT' };
+  wrapper.instance().handleIssueChange(updatedIssue);
+
+  expect(wrapper.state().openIssue).toBe(updatedIssue);
+});
+
 it('should handle createAfter query param with time', async () => {
   const fetchIssues = fetchIssuesMockFactory();
   const wrapper = shallowRender({
index 2cc7de55fe971c008287f1255635b08092866044..c70d98dd813dd4b13e9fb08c28398cd32890c05a 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { noop } from 'lodash';
 import * as React from 'react';
 import { getSources } from '../../../api/components';
+import Issue from '../../../components/issue/Issue';
 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
 import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
 import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
@@ -344,10 +346,19 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
   }
 
   render() {
-    const { branchLike, issue, issuesByLine, lastSnippetGroup, snippetGroup } = this.props;
+    const {
+      branchLike,
+      issue,
+      issuesByLine,
+      issuePopup,
+      lastSnippetGroup,
+      snippetGroup
+    } = this.props;
     const { additionalLines, loading, snippets } = this.state;
     const locations =
-      issue.component === snippetGroup.component.key ? locationsByLine([issue]) : {};
+      issue.component === snippetGroup.component.key && issue.textRange !== undefined
+        ? locationsByLine([issue])
+        : {};
 
     const fullyShown =
       snippets.length === 1 &&
@@ -373,6 +384,18 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
           onExpand={this.expandComponent}
           sourceViewerFile={snippetGroup.component}
         />
+        {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
+          <div className="padded-top padded-left padded-right">
+            <Issue
+              issue={issue}
+              onChange={this.props.onIssueChange}
+              onClick={noop}
+              onPopupToggle={this.props.onIssuePopupToggle}
+              openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined}
+              selected={true}
+            />
+          </div>
+        )}
         {snippetLines.map((snippet, index) => (
           <div id={`snippet-wrapper-${snippets[index].index}`} key={snippets[index].index}>
             {this.renderSnippet({
index 6132a637b9f55f1d65248023d383618ee6866ed3..7cbb22843f9deea2cb69c97384f22149a9ef4d5f 100644 (file)
@@ -22,6 +22,7 @@ import { range, times } from 'lodash';
 import * as React from 'react';
 import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { getSources } from '../../../../api/components';
+import Issue from '../../../../components/issue/Issue';
 import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
 import {
   mockFlowLocation,
@@ -105,6 +106,29 @@ it('should render correctly with flows', () => {
   expect(wrapper.state('snippets')[1]).toEqual({ index: 1, start: 69, end: 79 });
 });
 
+it('should render file-level issue correctly', () => {
+  // issue with secondary locations and no primary location
+  const issue = mockIssue(true, {
+    flows: [],
+    textRange: undefined
+  });
+
+  const wrapper = shallowRender({
+    issue,
+    snippetGroup: {
+      locations: [
+        mockFlowLocation({
+          component: issue.component,
+          textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
+        })
+      ],
+      ...mockSnippetsByComponent(issue.component, range(29, 39))
+    }
+  });
+
+  expect(wrapper.find(Issue).exists()).toBe(true);
+});
+
 it('should expand block', async () => {
   (getSources as jest.Mock).mockResolvedValueOnce(
     Object.values(mockSnippetsByComponent('a', range(6, 59)).sources)
index 9dcb749ea7f0fdea46dc49d9fd4a134f057f6e2b..6de0bb32650a39621542c604d459bc560418afb9 100644 (file)
@@ -176,7 +176,7 @@ describe('createSnippets', () => {
     expect(results[1]).toEqual({ index: 2, start: 37, end: 47 });
   });
 
-  it('should work for location with no textrange', () => {
+  it('should ignore location with no textrange', () => {
     const locations = [
       mockFlowLocation({
         textRange: { startLine: 85, startOffset: 2, endLine: 85, endOffset: 3 }
@@ -192,8 +192,8 @@ describe('createSnippets', () => {
       issue
     });
 
-    expect(results).toHaveLength(2);
-    expect(results[0]).toEqual({ index: 0, start: 1, end: 9 });
+    expect(results).toHaveLength(1);
+    expect(results[0]).toEqual({ index: 0, start: 80, end: 94 });
   });
 });
 
index 94e08e78d8fadd4af25242e5477ecfc382b8a2f8..a7b0c31d28c5c15bb901c957f120974ee07788c8 100644 (file)
@@ -73,7 +73,8 @@ export function createSnippets(params: {
   const { component, issue, locations } = params;
 
   const hasSecondaryLocations = issue.secondaryLocations.length > 0;
-  const addIssueLocation = hasSecondaryLocations && issue.component === component;
+  const addIssueLocation =
+    hasSecondaryLocations && issue.component === component && issue.textRange !== undefined;
 
   // For each location: compute its range, and then compare with other ranges
   // to merge snippets that collide.
@@ -144,25 +145,25 @@ export function groupLocationsByComponent(
   let currentGroup: T.SnippetGroup;
   const groups: T.SnippetGroup[] = [];
 
-  const addGroup = (loc: T.FlowLocation) => {
+  const addGroup = (componentKey: string) => {
     currentGroup = {
-      ...(components[loc.component] || unknownComponent(loc.component)),
+      ...(components[componentKey] || unknownComponent(componentKey)),
       locations: []
     };
     groups.push(currentGroup);
-    currentComponent = loc.component;
+    currentComponent = componentKey;
   };
 
   if (
     issue.secondaryLocations.length > 0 &&
     locations.every(loc => loc.component !== issue.component)
   ) {
-    addGroup(getPrimaryLocation(issue));
+    addGroup(issue.component);
   }
 
   locations.forEach((loc, index) => {
     if (loc.component !== currentComponent) {
-      addGroup(loc);
+      addGroup(loc.component);
     }
     loc.index = index;
     currentGroup.locations.push(loc);