From 9261d0a1d167022244856dd7949b57fa1f0b68f5 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 30 Nov 2020 18:03:18 +0100 Subject: [PATCH] SONAR-14120 Display file-level issues with multi-locations --- .../main/js/apps/issues/components/App.tsx | 3 ++- .../issues/components/__tests__/App-test.tsx | 14 +++++++++- .../ComponentSourceSnippetGroupViewer.tsx | 27 +++++++++++++++++-- ...ComponentSourceSnippetGroupViewer-test.tsx | 24 +++++++++++++++++ .../__tests__/utils-test.ts | 6 ++--- .../crossComponentSourceViewer/utils.ts | 13 ++++----- 6 files changed, 74 insertions(+), 13 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index 704fe4fdde3..5a0d3207965 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -797,7 +797,8 @@ export default class App extends React.PureComponent { 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 })); }; diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx index 5cb0f444f46..7f639106c24 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx @@ -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({ diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index 2cc7de55fe9..c70d98dd813 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -17,8 +17,10 @@ * 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 && ( +
+ +
+ )} {snippetLines.map((snippet, index) => (
{this.renderSnippet({ diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx index 6132a637b9f..7cbb22843f9 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetGroupViewer-test.tsx @@ -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) diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts index 9dcb749ea7f..6de0bb32650 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/utils-test.ts @@ -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 }); }); }); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts index 94e08e78d8f..a7b0c31d28c 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts @@ -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); -- 2.39.5