aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2019-05-02 17:50:21 +0200
committerSonarTech <sonartech@sonarsource.com>2019-05-07 20:21:26 +0200
commitf2f3ede233eca98118c026c7cf151145eb0dfa99 (patch)
tree536223c3b3ad7df40de1bb4483a41a9e7aa09d02
parent0e66ef03f6d1a86fa12f7b9574f6e9f9b36aa3a2 (diff)
downloadsonarqube-f2f3ede233eca98118c026c7cf151145eb0dfa99.tar.gz
sonarqube-f2f3ede233eca98118c026c7cf151145eb0dfa99.zip
SONAR-12076 Add duplication popup back to the new multi location issue flow
-rw-r--r--server/sonar-web/src/main/js/api/components.ts6
-rw-r--r--server/sonar-web/src/main/js/app/types.d.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx166
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx85
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap182
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css5
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx49
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap66
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts49
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts92
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts48
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.ts (renamed from server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx)0
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts (renamed from server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx)0
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.css1
21 files changed, 788 insertions, 126 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index 7b97f66111c..6141c5d879b 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -276,8 +276,10 @@ export function getSources(
return getJSON('/api/sources/lines', data).then(r => r.sources);
}
-export function getDuplications(data: { key: string } & T.BranchParameters): Promise<any> {
- return getJSON('/api/duplications/show', data);
+export function getDuplications(
+ data: { key: string } & T.BranchParameters
+): Promise<{ duplications: T.Duplication[]; files: T.Dict<T.DuplicatedFile> }> {
+ return getJSON('/api/duplications/show', data).catch(throwGlobalError);
}
export function getTests(
diff --git a/server/sonar-web/src/main/js/app/types.d.ts b/server/sonar-web/src/main/js/app/types.d.ts
index 5ec635aab23..27fde4e2538 100644
--- a/server/sonar-web/src/main/js/app/types.d.ts
+++ b/server/sonar-web/src/main/js/app/types.d.ts
@@ -257,7 +257,7 @@ declare namespace T {
}
export interface DuplicationBlock {
- _ref: string;
+ _ref?: string;
from: number;
size: number;
}
@@ -419,6 +419,13 @@ declare namespace T {
[line: number]: SourceLine;
}
+ export interface LinePopup {
+ index?: number;
+ line: number;
+ name: string;
+ open?: boolean;
+ }
+
export interface LoggedInUser extends CurrentUser {
avatar?: string;
email?: string;
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx
index 3e362317db0..d989e9f5a4a 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetViewer.tsx
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
import {
createSnippets,
expandSnippet,
@@ -26,11 +27,11 @@ import {
LINES_BELOW_LAST,
MERGE_DISTANCE
} from './utils';
-import { getSources } from '../../../api/components';
import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
import Line from '../../../components/SourceViewer/components/Line';
import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
+import { getSources } from '../../../api/components';
import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
import {
@@ -42,16 +43,25 @@ import { translate } from '../../../helpers/l10n';
interface Props {
branchLike: T.BranchLike | undefined;
+ duplications?: T.Duplication[];
+ duplicationsByLine?: { [line: number]: number[] };
highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
issue: T.Issue;
issuePopup?: { issue: string; name: string };
issuesByLine: T.IssuesByLine;
last: boolean;
+ linePopup?: T.LinePopup;
+ loadDuplications: (component: string, line: T.SourceLine) => void;
locations: T.FlowLocation[];
onIssueChange: (issue: T.Issue) => void;
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+ onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void;
onLocationSelect: (index: number) => void;
- renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+ renderDuplicationPopup: (
+ component: T.SourceViewerFile,
+ index: number,
+ line: number
+ ) => React.ReactNode;
scroll?: (element: HTMLElement) => void;
snippetGroup: T.SnippetGroup;
}
@@ -59,7 +69,6 @@ interface Props {
interface State {
additionalLines: { [line: number]: T.SourceLine };
highlightedSymbols: string[];
- linePopup?: { index?: number; line: number; name: string };
loading: boolean;
openIssuesByLine: T.Dict<boolean>;
snippets: T.SourceLine[][];
@@ -140,7 +149,6 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
return {
additionalLines: combinedLines,
- linePopup: undefined,
snippets: expandSnippet({
direction,
lines: { ...combinedLines, ...this.props.snippetGroup.sources },
@@ -163,7 +171,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
getSources({ key }).then(
lines => {
if (this.mounted) {
- this.setState({ linePopup: undefined, loading: false, snippets: [lines] });
+ this.setState({ loading: false, snippets: [lines] });
}
},
() => {
@@ -174,29 +182,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
);
};
- handleLinePopupToggle = ({
- index,
- line,
- name,
- open
- }: {
- index?: number;
- line: number;
- name: string;
- open?: boolean;
- }) => {
- this.setState((state: State) => {
- const samePopup =
- state.linePopup !== undefined &&
- state.linePopup.name === name &&
- state.linePopup.line === line &&
- state.linePopup.index === index;
- if (open !== false && !samePopup) {
- return { linePopup: { index, line, name } };
- } else if (open !== true && samePopup) {
- return { linePopup: undefined };
- }
- return null;
+ handleLinePopupToggle = (linePopup: T.LinePopup) => {
+ this.props.onLinePopupToggle({
+ ...linePopup,
+ component: this.props.snippetGroup.component.key
});
};
@@ -216,6 +205,14 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
this.setState({ highlightedSymbols });
};
+ loadDuplications = (line: T.SourceLine) => {
+ this.props.loadDuplications(this.props.snippetGroup.component.key, line);
+ };
+
+ renderDuplicationPopup = (index: number, line: number) => {
+ return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
+ };
+
renderLine({
index,
issuesForLine,
@@ -234,10 +231,12 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
verticalBuffer: number;
}) {
const { openIssuesByLine } = this.state;
-
const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
- const noop = () => {};
+ const { duplications, duplicationsByLine } = this.props;
+ const duplicationsCount = duplications ? duplications.length : 0;
+ const lineDuplications =
+ (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
@@ -246,11 +245,11 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
branchLike={undefined}
displayAllIssues={false}
displayCoverage={true}
- displayDuplications={false}
+ displayDuplications={!!line.duplicated}
displayIssues={!isSinkLine || issuesForLine.length > 1}
displayLocationMarkers={true}
- duplications={[]}
- duplicationsCount={0}
+ duplications={lineDuplications}
+ duplicationsCount={duplicationsCount}
highlighted={false}
highlightedLocationMessage={optimizeLocationMessage(
this.props.highlightedLocationMessage,
@@ -263,12 +262,12 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
key={line.line}
last={false}
line={line}
- linePopup={this.state.linePopup}
- loadDuplications={noop}
+ linePopup={this.props.linePopup}
+ loadDuplications={this.loadDuplications}
onIssueChange={this.props.onIssueChange}
onIssuePopupToggle={this.props.onIssuePopupToggle}
- onIssueSelect={noop}
- onIssueUnselect={noop}
+ onIssueSelect={() => {}}
+ onIssueUnselect={() => {}}
onIssuesClose={this.handleCloseIssues}
onIssuesOpen={this.handleOpenIssues}
onLinePopupToggle={this.handleLinePopupToggle}
@@ -276,7 +275,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
onSymbolClick={this.handleSymbolClick}
openIssues={openIssuesByLine[line.line]}
previousLine={index > 0 ? snippet[index - 1] : undefined}
- renderDuplicationPopup={this.props.renderDuplicationPopup}
+ renderDuplicationPopup={this.renderDuplicationPopup}
scroll={this.props.scroll}
secondaryIssueLocations={secondaryIssueLocations}
selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
@@ -359,7 +358,7 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
}
render() {
- const { branchLike, issue, issuesByLine, last, snippetGroup } = this.props;
+ const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props;
const { loading, snippets } = this.state;
const locations = locationsByLine([issue]);
@@ -369,7 +368,10 @@ export default class ComponentSourceSnippetViewer extends React.PureComponent<Pr
snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10);
return (
- <div className="component-source-container">
+ <div
+ className={classNames('component-source-container', {
+ 'source-duplications-expanded': duplications && duplications.length > 0
+ })}>
<SourceViewerHeaderSlim
branchLike={branchLike}
expandable={!fullyShown}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
index 92760bb3b22..8cdcbbdfd62 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewerWrapper.tsx
@@ -21,14 +21,20 @@ import * as React from 'react';
import ComponentSourceSnippetViewer from './ComponentSourceSnippetViewer';
import { groupLocationsByComponent } from './utils';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
+import { WorkspaceContext } from '../../../components/workspace/context';
import { getIssueFlowSnippets } from '../../../api/issues';
-import { issuesByComponentAndLine } from '../../../components/SourceViewer/helpers/indexing';
-
-interface State {
- components: T.Dict<T.SnippetsByComponent>;
- issuePopup?: { issue: string; name: string };
- loading: boolean;
-}
+import {
+ filterDuplicationBlocksByLine,
+ isDuplicationBlockInRemovedComponent,
+ getDuplicationBlocksForIndex
+} from '../../../components/SourceViewer/helpers/duplications';
+import {
+ duplicationsByLine,
+ issuesByComponentAndLine
+} from '../../../components/SourceViewer/helpers/indexing';
+import { getDuplications } from '../../../api/components';
+import { getBranchLikeQuery } from '../../../helpers/branches';
interface Props {
branchLike: T.Branch | T.PullRequest | undefined;
@@ -39,15 +45,25 @@ interface Props {
onIssueChange: (issue: T.Issue) => void;
onLoaded?: () => void;
onLocationSelect: (index: number) => void;
- renderDuplicationPopup: (index: number, line: number) => JSX.Element;
scroll?: (element: HTMLElement) => void;
selectedFlowIndex: number | undefined;
}
+interface State {
+ components: T.Dict<T.SnippetsByComponent>;
+ duplicatedFiles?: T.Dict<T.DuplicatedFile>;
+ duplications?: T.Duplication[];
+ duplicationsByLine: { [line: number]: number[] };
+ issuePopup?: { issue: string; name: string };
+ linePopup?: T.LinePopup & { component: string };
+ loading: boolean;
+}
+
export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
components: {},
+ duplicationsByLine: {},
loading: true
};
@@ -66,12 +82,39 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
this.mounted = false;
}
+ fetchDuplications = (component: string, line: T.SourceLine) => {
+ getDuplications({
+ key: component,
+ ...getBranchLikeQuery(this.props.branchLike)
+ }).then(
+ r => {
+ if (this.mounted) {
+ this.setState(state => ({
+ duplicatedFiles: r.files,
+ duplications: r.duplications,
+ duplicationsByLine: duplicationsByLine(r.duplications),
+ linePopup:
+ r.duplications.length === 1
+ ? { component, index: 0, line: line.line, name: 'duplications' }
+ : state.linePopup
+ }));
+ }
+ },
+ () => {}
+ );
+ };
+
fetchIssueFlowSnippets(issueKey: string) {
this.setState({ loading: true });
getIssueFlowSnippets(issueKey).then(
components => {
if (this.mounted) {
- this.setState({ components, issuePopup: undefined, loading: false });
+ this.setState({
+ components,
+ issuePopup: undefined,
+ linePopup: undefined,
+ loading: false
+ });
if (this.props.onLoaded) {
this.props.onLoaded();
}
@@ -98,8 +141,61 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
});
};
+ handleLinePopupToggle = ({
+ component,
+ index,
+ line,
+ name,
+ open
+ }: T.LinePopup & { component: string }) => {
+ this.setState((state: State) => {
+ const samePopup =
+ state.linePopup !== undefined &&
+ state.linePopup.line === line &&
+ state.linePopup.name === name &&
+ state.linePopup.component === component &&
+ state.linePopup.index === index;
+ if (open !== false && !samePopup) {
+ return { linePopup: { component, index, line, name } };
+ } else if (open !== true && samePopup) {
+ return { linePopup: undefined };
+ }
+ return null;
+ });
+ };
+
+ handleCloseLinePopup = () => {
+ this.setState({ linePopup: undefined });
+ };
+
+ renderDuplicationPopup = (component: T.SourceViewerFile, index: number, line: number) => {
+ const { duplicatedFiles, duplications } = this.state;
+
+ if (!component || !duplicatedFiles) {
+ return null;
+ }
+
+ const blocks = getDuplicationBlocksForIndex(duplications, index);
+
+ return (
+ <WorkspaceContext.Consumer>
+ {({ openComponent }) => (
+ <DuplicationPopup
+ blocks={filterDuplicationBlocksByLine(blocks, line)}
+ branchLike={this.props.branchLike}
+ duplicatedFiles={duplicatedFiles}
+ inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
+ onClose={this.handleCloseLinePopup}
+ openComponent={openComponent}
+ sourceViewerFile={component}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ };
+
render() {
- const { components, loading } = this.state;
+ const { loading } = this.state;
if (loading) {
return (
@@ -109,29 +205,43 @@ export default class CrossComponentSourceViewerWrapper extends React.PureCompone
);
}
+ const { components, duplications, duplicationsByLine, linePopup } = this.state;
const issuesByComponent = issuesByComponentAndLine(this.props.issues);
const locationsByComponent = groupLocationsByComponent(this.props.locations, components);
return (
<div>
- {locationsByComponent.map((g, i) => (
- <ComponentSourceSnippetViewer
- branchLike={this.props.branchLike}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- issue={this.props.issue}
- issuePopup={this.state.issuePopup}
- issuesByLine={issuesByComponent[g.component.key] || {}}
- key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`}
- last={i === locationsByComponent.length - 1}
- locations={g.locations || []}
- onIssueChange={this.props.onIssueChange}
- onIssuePopupToggle={this.handleIssuePopupToggle}
- onLocationSelect={this.props.onLocationSelect}
- renderDuplicationPopup={this.props.renderDuplicationPopup}
- scroll={this.props.scroll}
- snippetGroup={g}
- />
- ))}
+ {locationsByComponent.map((snippetGroup, i) => {
+ let componentProps = {};
+ if (linePopup && snippetGroup.component.key === linePopup.component) {
+ componentProps = {
+ duplications,
+ duplicationsByLine,
+ linePopup: { index: linePopup.index, line: linePopup.line, name: linePopup.name }
+ };
+ }
+ return (
+ <ComponentSourceSnippetViewer
+ branchLike={this.props.branchLike}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ issue={this.props.issue}
+ issuePopup={this.state.issuePopup}
+ issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
+ key={`${this.props.issue.key}-${this.props.selectedFlowIndex}-${i}`}
+ last={i === locationsByComponent.length - 1}
+ loadDuplications={this.fetchDuplications}
+ locations={snippetGroup.locations || []}
+ onIssueChange={this.props.onIssueChange}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onLinePopupToggle={this.handleLinePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ renderDuplicationPopup={this.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ snippetGroup={snippetGroup}
+ {...componentProps}
+ />
+ );
+ })}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx
index 980df641334..6b194e14452 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/ComponentSourceSnippetViewer-test.tsx
@@ -118,6 +118,55 @@ it('should handle symbol highlighting', () => {
expect(wrapper.state('highlightedSymbols')).toEqual(['foo']);
});
+it('should correctly handle lines actions', () => {
+ const snippetGroup: T.SnippetGroup = {
+ locations: [
+ mockFlowLocation({
+ component: 'a',
+ textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
+ }),
+ mockFlowLocation({
+ component: 'a',
+ textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 }
+ })
+ ],
+ ...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56])
+ };
+ const loadDuplications = jest.fn();
+ const onLinePopupToggle = jest.fn();
+ const renderDuplicationPopup = jest.fn();
+
+ const wrapper = shallowRender({
+ loadDuplications,
+ onLinePopupToggle,
+ renderDuplicationPopup,
+ snippetGroup
+ });
+
+ const line = mockSourceLine();
+ wrapper
+ .find('Line')
+ .first()
+ .prop<Function>('loadDuplications')(line);
+ expect(loadDuplications).toHaveBeenCalledWith('a', line);
+
+ wrapper
+ .find('Line')
+ .first()
+ .prop<Function>('onLinePopupToggle')({ line: 13, name: 'foo' });
+ expect(onLinePopupToggle).toHaveBeenCalledWith({ component: 'a', line: 13, name: 'foo' });
+
+ wrapper
+ .find('Line')
+ .first()
+ .prop<Function>('renderDuplicationPopup')(1, 13);
+ expect(renderDuplicationPopup).toHaveBeenCalledWith(
+ mockSourceViewerFile({ key: 'a', path: 'a' }),
+ 1,
+ 13
+ );
+});
+
function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {}) {
const snippetGroup: T.SnippetGroup = {
component: mockSourceViewerFile(),
@@ -127,13 +176,18 @@ function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {
return shallow<ComponentSourceSnippetViewer>(
<ComponentSourceSnippetViewer
branchLike={mockMainBranch()}
+ duplications={undefined}
+ duplicationsByLine={undefined}
highlightedLocationMessage={{ index: 0, text: '' }}
issue={mockIssue()}
issuesByLine={{}}
last={false}
+ linePopup={undefined}
+ loadDuplications={jest.fn()}
locations={[]}
onIssueChange={jest.fn()}
onIssuePopupToggle={jest.fn()}
+ onLinePopupToggle={jest.fn()}
onLocationSelect={jest.fn()}
renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx
index 634537fd178..4d283b416d1 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/CrossComponentSourceViewerWrapper-test.tsx
@@ -20,31 +20,46 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper';
-import { mockIssue, mockSourceViewerFile } from '../../../../helpers/testMocks';
+import {
+ mockFlowLocation,
+ mockIssue,
+ mockSnippetsByComponent,
+ mockSourceLine,
+ mockSourceViewerFile
+} from '../../../../helpers/testMocks';
import { waitAndUpdate } from '../../../../helpers/testUtils';
import { getIssueFlowSnippets } from '../../../../api/issues';
+import { getDuplications } from '../../../../api/components';
jest.mock('../../../../api/issues', () => {
- const { mockSourceViewerFile } = require.requireActual('../../../../helpers/testMocks');
+ const { mockSnippetsByComponent } = require.requireActual('../../../../helpers/testMocks');
return {
- getIssueFlowSnippets: jest.fn().mockResolvedValue([mockSourceViewerFile()])
+ getIssueFlowSnippets: jest.fn().mockResolvedValue({ 'main.js': mockSnippetsByComponent() })
};
});
+jest.mock('../../../../api/components', () => ({
+ getDuplications: jest.fn().mockResolvedValue({})
+}));
+
beforeEach(() => {
jest.clearAllMocks();
});
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
+it('should render correctly', async () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
});
it('Should fetch data', async () => {
const wrapper = shallowRender();
wrapper.instance().fetchIssueFlowSnippets('124');
await waitAndUpdate(wrapper);
- expect(getIssueFlowSnippets).toBeCalled();
- expect(wrapper.state('components')).toEqual([mockSourceViewerFile()]);
+ expect(getIssueFlowSnippets).toHaveBeenCalledWith('1');
+ expect(wrapper.state('components')).toEqual({ 'main.js': mockSnippetsByComponent() });
(getIssueFlowSnippets as jest.Mock).mockClear();
wrapper.setProps({ issue: mockIssue(true, { key: 'foo' }) });
@@ -62,18 +77,68 @@ it('should handle issue popup', () => {
expect(wrapper.state('issuePopup')).toBeUndefined();
});
+it('should handle line popup', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ const linePopup = { component: 'foo', index: 0, line: 16, name: 'b.tsx' };
+ wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(linePopup);
+ expect(wrapper.state('linePopup')).toEqual(linePopup);
+
+ wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(linePopup);
+ expect(wrapper.state('linePopup')).toEqual(undefined);
+
+ const openLinePopup = { ...linePopup, open: true };
+ wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(openLinePopup);
+ wrapper.find('ComponentSourceSnippetViewer').prop<Function>('onLinePopupToggle')(openLinePopup);
+ expect(wrapper.state('linePopup')).toEqual(linePopup);
+});
+
+it('should handle duplication popup', async () => {
+ const files = { b: { key: 'b', name: 'B.tsx', project: 'foo', projectName: 'Foo' } };
+ const duplications = [{ blocks: [{ _ref: '1', from: 1, size: 2 }] }];
+ (getDuplications as jest.Mock).mockResolvedValueOnce({ duplications, files });
+
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ wrapper.find('ComponentSourceSnippetViewer').prop<Function>('loadDuplications')(
+ 'foo',
+ mockSourceLine()
+ );
+
+ await waitAndUpdate(wrapper);
+ expect(getDuplications).toHaveBeenCalledWith({ key: 'foo' });
+ expect(wrapper.state('duplicatedFiles')).toEqual(files);
+ expect(wrapper.state('duplications')).toEqual(duplications);
+ expect(wrapper.state('duplicationsByLine')).toEqual({ '1': [0], '2': [0] });
+ expect(wrapper.state('linePopup')).toEqual({
+ component: 'foo',
+ index: 0,
+ line: 16,
+ name: 'duplications'
+ });
+
+ expect(
+ wrapper.find('ComponentSourceSnippetViewer').prop<Function>('renderDuplicationPopup')(
+ mockSourceViewerFile(),
+ 0,
+ 16
+ )
+ ).toMatchSnapshot();
+});
+
function shallowRender(props: Partial<CrossComponentSourceViewerWrapper['props']> = {}) {
return shallow<CrossComponentSourceViewerWrapper>(
<CrossComponentSourceViewerWrapper
branchLike={undefined}
highlightedLocationMessage={undefined}
- issue={mockIssue(true)}
+ issue={mockIssue(true, { key: '1' })}
issues={[]}
- locations={[]}
+ locations={[mockFlowLocation()]}
onIssueChange={jest.fn()}
onLoaded={jest.fn()}
onLocationSelect={jest.fn()}
- renderDuplicationPopup={jest.fn()}
scroll={jest.fn()}
selectedFlowIndex={0}
{...props}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap
index 9283b3f518f..1aeb92a076d 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/__snapshots__/CrossComponentSourceViewerWrapper-test.tsx.snap
@@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`should handle duplication popup 1`] = `
+<Context.Consumer>
+ [Function]
+</Context.Consumer>
+`;
+
exports[`should render correctly 1`] = `
<div>
<DeferredSpinner
@@ -7,3 +13,179 @@ exports[`should render correctly 1`] = `
/>
</div>
`;
+
+exports[`should render correctly 2`] = `
+<div>
+ <ComponentSourceSnippetViewer
+ issue={
+ Object {
+ "actions": Array [],
+ "component": "main.js",
+ "componentLongName": "main.js",
+ "componentQualifier": "FIL",
+ "componentUuid": "foo1234",
+ "creationDate": "2017-03-01T09:36:01+0100",
+ "flows": Array [
+ Array [
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ],
+ Array [
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ],
+ ],
+ "fromHotspot": false,
+ "key": "1",
+ "line": 25,
+ "message": "Reduce the number of conditional operators (4) used in the expression",
+ "organization": "myorg",
+ "project": "myproject",
+ "projectKey": "foo",
+ "projectName": "Foo",
+ "projectOrganization": "org",
+ "rule": "javascript:S1067",
+ "ruleName": "foo",
+ "secondaryLocations": Array [
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ Object {
+ "component": "main.js",
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ],
+ "severity": "MAJOR",
+ "status": "OPEN",
+ "textRange": Object {
+ "endLine": 26,
+ "endOffset": 15,
+ "startLine": 25,
+ "startOffset": 0,
+ },
+ "transitions": Array [],
+ "type": "BUG",
+ }
+ }
+ issuesByLine={Object {}}
+ key="1-0-0"
+ last={true}
+ loadDuplications={[Function]}
+ locations={
+ Array [
+ Object {
+ "component": "main.js",
+ "index": 0,
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ]
+ }
+ onIssueChange={[MockFunction]}
+ onIssuePopupToggle={[Function]}
+ onLinePopupToggle={[Function]}
+ onLocationSelect={[MockFunction]}
+ renderDuplicationPopup={[Function]}
+ scroll={[MockFunction]}
+ snippetGroup={
+ Object {
+ "component": Object {
+ "key": "main.js",
+ "measures": Object {
+ "coverage": "85.2",
+ "duplicationDensity": "1.0",
+ "issues": "12",
+ "lines": "56",
+ },
+ "path": "main.js",
+ "project": "my-project",
+ "projectName": "MyProject",
+ "q": "FIL",
+ "uuid": "foo-bar",
+ },
+ "locations": Array [
+ Object {
+ "component": "main.js",
+ "index": 0,
+ "textRange": Object {
+ "endLine": 2,
+ "endOffset": 2,
+ "startLine": 1,
+ "startOffset": 1,
+ },
+ },
+ ],
+ "sources": Object {
+ "16": Object {
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
+ "coverageStatus": "covered",
+ "coveredConditions": 2,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
+ },
+ },
+ }
+ }
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
index 1cb4a7e4973..85d3c2e3460 100644
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ b/server/sonar-web/src/main/js/apps/issues/styles.css
@@ -254,8 +254,11 @@
text-align: left;
cursor: pointer;
}
-.snippet > .expand-block:hover {
+.snippet > .expand-block:hover,
+.snippet > .expand-block:focus,
+.snippet > .expand-block:active {
color: var(--darkBlue);
+ outline: none;
}
.snippet > .expand-block-above {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII=');
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
index 10341ecd3b1..3abf5f6e0d6 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx
@@ -28,11 +28,17 @@ import DuplicationPopup from './components/DuplicationPopup';
import defaultLoadIssues from './helpers/loadIssues';
import getCoverageStatus from './helpers/getCoverageStatus';
import {
+ filterDuplicationBlocksByLine,
+ getDuplicationBlocksForIndex,
+ isDuplicationBlockInRemovedComponent
+} from './helpers/duplications';
+import {
duplicationsByLine,
issuesByLine,
locationsByLine,
symbolsByLine
} from './helpers/indexing';
+import { Alert } from '../ui/Alert';
import {
getComponentData,
getComponentForSourceViewer,
@@ -41,7 +47,6 @@ import {
} from '../../api/components';
import { isSameBranchLike, getBranchLikeQuery } from '../../helpers/branches';
import { translate } from '../../helpers/l10n';
-import { Alert } from '../ui/Alert';
import { WorkspaceContext } from '../workspace/context';
import './styles.css';
@@ -97,7 +102,7 @@ interface State {
issuePopup?: { issue: string; name: string };
issues?: T.Issue[];
issuesByLine: { [line: number]: T.Issue[] };
- linePopup?: { index?: number; line: number; name: string };
+ linePopup?: T.LinePopup;
loading: boolean;
loadingSourcesAfter: boolean;
loadingSourcesBefore: boolean;
@@ -495,17 +500,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
);
};
- handleLinePopupToggle = ({
- index,
- line,
- name,
- open
- }: {
- index?: number;
- line: number;
- name: string;
- open?: boolean;
- }) => {
+ handleLinePopupToggle = ({ index, line, name, open }: T.LinePopup) => {
this.setState((state: State) => {
const samePopup =
state.linePopup !== undefined &&
@@ -587,34 +582,20 @@ export default class SourceViewerBase extends React.PureComponent<Props, State>
renderDuplicationPopup = (index: number, line: number) => {
const { component, duplicatedFiles, duplications } = this.state;
- if (!component || !duplicatedFiles) return <></>;
-
- const duplication = duplications && duplications[index];
- let blocks = (duplication && duplication.blocks) || [];
- /* eslint-disable no-underscore-dangle */
- const inRemovedComponent = blocks.some(b => b._ref === undefined);
- let foundOne = false;
- blocks = blocks.filter(b => {
- const outOfBounds = b.from > line || b.from + b.size < line;
- const currentFile = b._ref === '1';
- const shouldDisplayForCurrentFile = outOfBounds || foundOne;
- const shouldDisplay = !currentFile || shouldDisplayForCurrentFile;
- const isOk = b._ref !== undefined && shouldDisplay;
- if (b._ref === '1' && !outOfBounds) {
- foundOne = true;
- }
- return isOk;
- });
- /* eslint-enable no-underscore-dangle */
+ if (!component || !duplicatedFiles) {
+ return null;
+ }
+
+ const blocks = getDuplicationBlocksForIndex(duplications, index);
return (
<WorkspaceContext.Consumer>
{({ openComponent }) => (
<DuplicationPopup
- blocks={blocks}
+ blocks={filterDuplicationBlocksByLine(blocks, line)}
branchLike={this.props.branchLike}
duplicatedFiles={duplicatedFiles}
- inRemovedComponent={inRemovedComponent}
+ inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
onClose={this.closeLinePopup}
openComponent={openComponent}
sourceViewerFile={component}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
index 04ecb86b671..9afe3118d43 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
@@ -56,7 +56,7 @@ interface Props {
issuePopup: { issue: string; name: string } | undefined;
issues: T.Issue[] | undefined;
issuesByLine: { [line: number]: T.Issue[] };
- linePopup: { index?: number; line: number; name: string } | undefined;
+ linePopup: T.LinePopup | undefined;
loadDuplications: (line: T.SourceLine) => void;
loadingSourcesAfter: boolean;
loadingSourcesBefore: boolean;
@@ -68,11 +68,11 @@ interface Props {
onIssueSelect: (issueKey: string) => void;
onIssuesOpen: (line: T.SourceLine) => void;
onIssueUnselect: () => void;
- onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+ onLinePopupToggle: (linePopup: T.LinePopup) => void;
onLocationSelect: ((index: number) => void) | undefined;
onSymbolClick: (symbols: string[]) => void;
openIssuesByLine: { [line: number]: boolean };
- renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+ renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
scroll?: (element: HTMLElement) => void;
selectedIssue: string | undefined;
sources: T.SourceLine[];
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
index 79e938a542c..9dfa67cf3a0 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx
@@ -46,9 +46,9 @@ interface Props {
issues: T.Issue[];
last: boolean;
line: T.SourceLine;
- linePopup: { index?: number; line: number; name: string } | undefined;
+ linePopup: T.LinePopup | undefined;
loadDuplications: (line: T.SourceLine) => void;
- onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+ onLinePopupToggle: (linePopup: T.LinePopup) => void;
onIssueChange: (issue: T.Issue) => void;
onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void;
onIssuesClose: (line: T.SourceLine) => void;
@@ -59,7 +59,7 @@ interface Props {
onSymbolClick: (symbols: string[]) => void;
openIssues: boolean;
previousLine: T.SourceLine | undefined;
- renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+ renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
scroll?: (element: HTMLElement) => void;
secondaryIssueLocations: T.LinearIssueLocation[];
selectedIssue: string | undefined;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
index 334b97b260a..cf75f6912a3 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
@@ -27,9 +27,9 @@ interface Props {
duplicated: boolean;
index: number;
line: T.SourceLine;
- onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+ onPopupToggle: (linePopup: T.LinePopup) => void;
popupOpen: boolean;
- renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+ renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
}
export default class LineDuplicationBlock extends React.PureComponent<Props> {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
index 5d90fb7acdd..137f14ba526 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx
@@ -23,7 +23,7 @@ import Toggler from '../../controls/Toggler';
interface Props {
line: T.SourceLine;
- onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+ onPopupToggle: (linePopup: T.LinePopup) => void;
popupOpen: boolean;
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
index 8f549955150..151dc7422b0 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
@@ -23,7 +23,7 @@ import Toggler from '../../controls/Toggler';
interface Props {
line: T.SourceLine;
- onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void;
+ onPopupToggle: (linePopup: T.LinePopup) => void;
popupOpen: boolean;
previousLine: T.SourceLine | undefined;
}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap
new file mode 100644
index 00000000000..2e6635c4494
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`loadIssues should load issues 1`] = `
+Array [
+ Object {
+ "actions": Array [
+ "set_tags",
+ "comment",
+ "assign",
+ ],
+ "assignee": "luke",
+ "assigneeActive": true,
+ "assigneeAvatar": "lukavatar",
+ "assigneeLogin": "luke",
+ "assigneeName": "Luke",
+ "author": "luke@sonarsource.com",
+ "comments": Array [],
+ "component": "foo.java",
+ "componentEnabled": true,
+ "componentKey": "foo.java",
+ "componentLongName": "Foo.java",
+ "componentName": "foo.java",
+ "componentOrganization": "default-organization",
+ "componentPath": "/foo.java",
+ "componentQualifier": "FIL",
+ "creationDate": "2016-08-15T15:25:38+0200",
+ "flows": Array [],
+ "fromHotspot": true,
+ "hash": "78417dcee7ba927b7e7c9161e29e02b8",
+ "key": "AWaqVGl3tut9VbnJvk6M",
+ "line": 62,
+ "message": "Make sure this file handling is safe here.",
+ "organization": "default-organization",
+ "project": "org.sonarsource.java:java",
+ "projectEnabled": true,
+ "projectKey": "org.sonarsource.java:java",
+ "projectLongName": "SonarJava",
+ "projectName": "SonarJava",
+ "projectOrganization": "default-organization",
+ "projectQualifier": "TRK",
+ "rule": "squid:S4797",
+ "ruleKey": "squid:S4797",
+ "ruleLang": "java",
+ "ruleLangName": "Java",
+ "ruleName": "Handling files is security-sensitive",
+ "ruleStatus": "READY",
+ "secondaryLocations": Array [],
+ "status": "OPEN",
+ "tags": Array [
+ "cert",
+ "cwe",
+ "owasp-a1",
+ "owasp-a3",
+ ],
+ "textRange": Object {
+ "endLine": 62,
+ "endOffset": 96,
+ "startLine": 62,
+ "startOffset": 93,
+ },
+ "transitions": Array [],
+ "type": "SECURITY_HOTSPOT",
+ "updateDate": "2018-10-25T10:23:08+0200",
+ },
+]
+`;
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts
new file mode 100644
index 00000000000..03512c6330d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/duplications-test.ts
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import {
+ getDuplicationBlocksForIndex,
+ isDuplicationBlockInRemovedComponent
+} from '../duplications';
+
+describe('getDuplicationBlocksForIndex', () => {
+ it('should return duplications blocks', () => {
+ const blocks = [{ _ref: '0', from: 2, size: 2 }];
+ expect(getDuplicationBlocksForIndex([{ blocks }], 0)).toBe(blocks);
+ expect(getDuplicationBlocksForIndex([{ blocks }], 5)).toEqual([]);
+ expect(getDuplicationBlocksForIndex(undefined, 5)).toEqual([]);
+ });
+});
+
+describe('isDuplicationBlockInRemovedComponent', () => {
+ it('should ', () => {
+ expect(
+ isDuplicationBlockInRemovedComponent([
+ { _ref: '0', from: 2, size: 2 },
+ { _ref: '0', from: 3, size: 1 }
+ ])
+ ).toBe(false);
+ expect(
+ isDuplicationBlockInRemovedComponent([
+ { _ref: undefined, from: 2, size: 2 },
+ { _ref: '0', from: 3, size: 1 }
+ ])
+ ).toBe(true);
+ });
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts
new file mode 100644
index 00000000000..932eec9688b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/loadIssues-test.ts
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import loadIssues from '../loadIssues';
+import { mockMainBranch } from '../../../../helpers/testMocks';
+
+jest.mock('../../../../api/issues', () => ({
+ searchIssues: jest.fn().mockResolvedValue({
+ paging: { pageIndex: 1, pageSize: 500, total: 1 },
+ effortTotal: 15,
+ debtTotal: 15,
+ issues: [
+ {
+ key: 'AWaqVGl3tut9VbnJvk6M',
+ rule: 'squid:S4797',
+ component: 'foo.java',
+ project: 'org.sonarsource.java:java',
+ line: 62,
+ hash: '78417dcee7ba927b7e7c9161e29e02b8',
+ textRange: { startLine: 62, endLine: 62, startOffset: 93, endOffset: 96 },
+ flows: [],
+ status: 'OPEN',
+ message: 'Make sure this file handling is safe here.',
+ assignee: 'luke',
+ author: 'luke@sonarsource.com',
+ tags: ['cert', 'cwe', 'owasp-a1', 'owasp-a3'],
+ transitions: [],
+ actions: ['set_tags', 'comment', 'assign'],
+ comments: [],
+ creationDate: '2016-08-15T15:25:38+0200',
+ updateDate: '2018-10-25T10:23:08+0200',
+ type: 'SECURITY_HOTSPOT',
+ organization: 'default-organization',
+ fromHotspot: true
+ }
+ ],
+ components: [
+ {
+ organization: 'default-organization',
+ key: 'org.sonarsource.java:java',
+ enabled: true,
+ qualifier: 'TRK',
+ name: 'SonarJava',
+ longName: 'SonarJava'
+ },
+ {
+ organization: 'default-organization',
+ key: 'foo.java',
+ enabled: true,
+ qualifier: 'FIL',
+ name: 'foo.java',
+ longName: 'Foo.java',
+ path: '/foo.java'
+ }
+ ],
+ rules: [
+ {
+ key: 'squid:S4797',
+ name: 'Handling files is security-sensitive',
+ lang: 'java',
+ status: 'READY',
+ langName: 'Java'
+ }
+ ],
+ users: [{ login: 'luke', name: 'Luke', avatar: 'lukavatar', active: true }],
+ languages: [{ key: 'java', name: 'Java' }],
+ facets: []
+ })
+}));
+
+describe('loadIssues', () => {
+ it('should load issues', async () => {
+ const result = await loadIssues('foo.java', 1, 500, mockMainBranch());
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts
new file mode 100644
index 00000000000..90954569c96
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/duplications.ts
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+// TODO Test this function, but I don't get the logic behind it
+export function filterDuplicationBlocksByLine(blocks: T.DuplicationBlock[], line: number) {
+ /* eslint-disable no-underscore-dangle */
+ let foundOne = false;
+ return blocks.filter(b => {
+ const outOfBounds = b.from > line || b.from + b.size < line;
+ const currentFile = b._ref === '1';
+ const shouldDisplayForCurrentFile = outOfBounds || foundOne;
+ const shouldDisplay = !currentFile || shouldDisplayForCurrentFile;
+ const isOk = b._ref !== undefined && shouldDisplay;
+ if (b._ref === '1' && !outOfBounds) {
+ foundOne = true;
+ }
+ return isOk;
+ });
+ /* eslint-enable no-underscore-dangle */
+}
+
+export function getDuplicationBlocksForIndex(
+ duplications: T.Duplication[] | undefined,
+ index: number
+) {
+ return (duplications && duplications[index] && duplications[index].blocks) || [];
+}
+
+export function isDuplicationBlockInRemovedComponent(blocks: T.DuplicationBlock[]) {
+ return blocks.some(b => b._ref === undefined); // eslint-disable-line no-underscore-dangle
+}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.ts
index 2315af1115d..2315af1115d 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.ts
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts
index 0bf83942ced..0bf83942ced 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css
index 71c39dd6915..06861fd5599 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.css
+++ b/server/sonar-web/src/main/js/components/issue/Issue.css
@@ -31,6 +31,7 @@
.issue.selected {
box-shadow: none;
+ outline: none;
border: 2px solid var(--blue) !important;
}