aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>2023-06-08 10:21:56 +0200
committersonartech <sonartech@sonarsource.com>2023-06-09 20:03:10 +0000
commitfadf1df93389c41e8ccd836541161d688c6742ca (patch)
treedbe9f6afd70999ad791ff3ea26546c44d34482e3 /server
parent967bf884a9d329be91b0f9ee9e3b7a73229ec542 (diff)
downloadsonarqube-fadf1df93389c41e8ccd836541161d688c6742ca.tar.gz
sonarqube-fadf1df93389c41e8ccd836541161d688c6742ca.zip
SONAR-19489 Add coverage/new code highlights/labels to the code viewer
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts15
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx148
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx10
-rw-r--r--server/sonar-web/src/main/js/helpers/code-viewer.ts42
5 files changed, 158 insertions, 59 deletions
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 92f3c466f86..4b2eda9d13d 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
@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import { sortBy } from 'lodash';
+import { decorateWithUnderlineFlags } from '../../../helpers/code-viewer';
import { isDefined } from '../../../helpers/types';
import { ComponentQualifier } from '../../../types/component';
import { ReviewHistoryElement, ReviewHistoryType } from '../../../types/security-hotspots';
@@ -138,19 +140,6 @@ export function createSnippets(params: {
return hasSecondaryLocations ? ranges.sort((a, b) => a.start - b.start) : ranges;
}
-function decorateWithUnderlineFlags(line: SourceLine, sourcesMap: LineMap) {
- const previousLine = sourcesMap[line.line - 1];
- const decoratedLine = { ...line };
- if (isDefined(line.coverageStatus)) {
- decoratedLine.coverageBlock =
- line.coverageStatus === previousLine?.coverageStatus ? previousLine.coverageBlock : line.line;
- }
- if (line.isNew) {
- decoratedLine.newCodeBlock = previousLine?.isNew ? previousLine.newCodeBlock : line.line;
- }
- return decoratedLine;
-}
-
export function linesForSnippets(snippets: Snippet[], componentLines: LineMap) {
return snippets.reduce<Array<{ snippet: SourceLine[]; sourcesMap: LineMap }>>((acc, snippet) => {
const snippetSources = [];
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 9988a9f1903..95ccad809f2 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx
@@ -17,10 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { SonarCodeColorizer } from 'design-system/lib';
-import { noop } from 'lodash';
+
+import { SonarCodeColorizer } from 'design-system';
import * as React from 'react';
import { Button } from '../../components/controls/buttons';
+import { decorateWithUnderlineFlags } from '../../helpers/code-viewer';
import { translate } from '../../helpers/l10n';
import { BranchLike } from '../../types/branch-like';
import { MetricKey } from '../../types/metrics';
@@ -28,6 +29,7 @@ import {
Duplication,
FlowLocation,
Issue,
+ LineMap,
LinearIssueLocation,
SourceLine,
} from '../../types/types';
@@ -36,7 +38,7 @@ import LineIssuesList from './components/LineIssuesList';
import { getSecondaryIssueLocationsForLine } from './helpers/issueLocations';
import { optimizeHighlightedSymbols, optimizeLocationMessage } from './helpers/lines';
-const EMPTY_ARRAY: any[] = [];
+const EMPTY_ARRAY: unknown[] = [];
const ZERO_LINE = {
code: '',
@@ -45,6 +47,11 @@ const ZERO_LINE = {
line: 0,
};
+interface State {
+ decoratedLinesMap: LineMap;
+ hoveredLine?: SourceLine;
+}
+
interface Props {
branchLike: BranchLike | undefined;
displayAllIssues?: boolean;
@@ -68,6 +75,7 @@ interface Props {
loadingSourcesBefore: boolean;
loadSourcesAfter: () => void;
loadSourcesBefore: () => void;
+ metricKey?: string;
onIssueChange: (issue: Issue) => void;
onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
onIssuesClose: (line: SourceLine) => void;
@@ -78,21 +86,42 @@ interface Props {
onSymbolClick: (symbols: string[]) => void;
openIssuesByLine: { [line: number]: boolean };
renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
- metricKey?: string;
selectedIssue: string | undefined;
sources: SourceLine[];
symbolsByLine: { [line: number]: string[] };
}
-export default class SourceViewerCode extends React.PureComponent<Props> {
+export default class SourceViewerCode extends React.PureComponent<Props, State> {
firstUncoveredLineFound = false;
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ decoratedLinesMap: this.getDecoratedLinesMap(props.sources),
+ hoveredLine: undefined,
+ };
+ }
+
componentDidUpdate(prevProps: Props) {
if (this.props.metricKey !== prevProps.metricKey) {
this.firstUncoveredLineFound = false;
}
+
+ if (this.props.sources !== prevProps.sources) {
+ this.setState({
+ decoratedLinesMap: this.getDecoratedLinesMap(this.props.sources),
+ });
+ }
}
+ getDecoratedLinesMap = (sources: SourceLine[]) =>
+ sources.reduce((map: LineMap, line: SourceLine) => {
+ map[line.line] = decorateWithUnderlineFlags(line, map);
+
+ return map;
+ }, {});
+
getDuplicationsForLine = (line: SourceLine): number[] => {
return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
};
@@ -105,38 +134,61 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY;
};
+ onLineMouseEnter = (hoveredLineNumber: number) =>
+ this.setState(({ decoratedLinesMap }) => ({
+ hoveredLine: decoratedLinesMap[hoveredLineNumber],
+ }));
+
+ onLineMouseLeave = (leftLineNumber: number) =>
+ this.setState(({ hoveredLine }) => ({
+ hoveredLine: hoveredLine?.line === leftLineNumber ? undefined : hoveredLine,
+ }));
+
renderLine = ({
- line,
- index,
displayCoverage,
displayDuplications,
displayIssues,
+ index,
+ line,
}: {
- line: SourceLine;
- index: number;
displayCoverage: boolean;
displayDuplications: boolean;
displayIssues: boolean;
+ index: number;
+ line: SourceLine;
}) => {
+ const { hoveredLine } = this.state;
+
const {
- highlightedLocationMessage,
- selectedIssue,
- openIssuesByLine,
- issueLocationsByLine,
+ branchLike,
displayAllIssues,
+ displayIssueLocationsCount,
+ displayIssueLocationsLink,
+ displayLocationMarkers,
+ duplications,
+ highlightedLine,
+ highlightedLocationMessage,
highlightedLocations,
+ highlightedSymbols,
+ issueLocationsByLine,
+ issuePopup,
metricKey,
+ openIssuesByLine,
+ selectedIssue,
sources,
+ symbolsByLine,
} = this.props;
const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, highlightedLocations);
- const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0;
+ const duplicationsCount = duplications?.length ?? 0;
const issuesForLine = this.getIssuesForLine(line);
- const firstLineNumber = sources && sources.length ? sources[0].line : 0;
+
+ const firstLineNumber = sources?.length ? sources[0].line : 0;
let scrollToUncoveredLine = false;
+
if (
!this.firstUncoveredLineFound &&
displayCoverage &&
@@ -146,62 +198,69 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
scrollToUncoveredLine =
(metricKey === MetricKey.new_uncovered_lines && line.isNew) ||
metricKey === MetricKey.uncovered_lines;
+
this.firstUncoveredLineFound = scrollToUncoveredLine;
}
+ const displayCoverageUnderline = !!(
+ hoveredLine?.coverageBlock && hoveredLine.coverageBlock === line.coverageBlock
+ );
+
return (
<Line
- displayAllIssues={this.props.displayAllIssues}
- displayNewCodeUnderline={false}
- displayCoverageUnderline={false}
- onLineMouseEnter={noop}
- onLineMouseLeave={noop}
+ displayAllIssues={displayAllIssues}
displayCoverage={displayCoverage}
+ displayCoverageUnderline={displayCoverageUnderline}
displayDuplications={displayDuplications}
displayIssues={displayIssues}
- displayLocationMarkers={this.props.displayLocationMarkers}
+ displayLocationMarkers={displayLocationMarkers}
+ displayNewCodeUnderline={hoveredLine?.newCodeBlock === line.line}
displaySCM={sources.length > 0}
duplications={this.getDuplicationsForLine(line)}
duplicationsCount={duplicationsCount}
firstLineNumber={firstLineNumber}
- highlighted={line.line === this.props.highlightedLine}
+ highlighted={line.line === highlightedLine}
highlightedLocationMessage={optimizeLocationMessage(
highlightedLocationMessage,
secondaryIssueLocations
)}
highlightedSymbols={optimizeHighlightedSymbols(
- this.props.symbolsByLine[line.line],
- this.props.highlightedSymbols
+ symbolsByLine[line.line],
+ highlightedSymbols
)}
issueLocations={this.getIssueLocationsForLine(line)}
issues={issuesForLine}
key={line.line || line.code}
line={line}
loadDuplications={this.props.loadDuplications}
- onIssueSelect={this.props.onIssueSelect}
- onIssueUnselect={this.props.onIssueUnselect}
onIssuesClose={this.props.onIssuesClose}
+ onIssueSelect={this.props.onIssueSelect}
onIssuesOpen={this.props.onIssuesOpen}
+ onIssueUnselect={this.props.onIssueUnselect}
+ onLineMouseEnter={this.onLineMouseEnter}
+ onLineMouseLeave={this.onLineMouseLeave}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
- openIssues={this.props.openIssuesByLine[line.line] || false}
+ openIssues={openIssuesByLine[line.line] || false}
previousLine={index > 0 ? sources[index - 1] : undefined}
renderDuplicationPopup={this.props.renderDuplicationPopup}
scrollToUncoveredLine={scrollToUncoveredLine}
secondaryIssueLocations={secondaryIssueLocations}
>
<LineIssuesList
- displayWhyIsThisAnIssue
+ branchLike={branchLike}
displayAllIssues={displayAllIssues}
+ displayIssueLocationsCount={displayIssueLocationsCount}
+ displayIssueLocationsLink={displayIssueLocationsLink}
+ displayWhyIsThisAnIssue
issueLocationsByLine={issueLocationsByLine}
+ issuePopup={issuePopup}
issuesForLine={issuesForLine}
line={line}
- openIssuesByLine={openIssuesByLine}
- branchLike={this.props.branchLike}
- issuePopup={this.props.issuePopup}
onIssueChange={this.props.onIssueChange}
onIssueClick={this.props.onIssueSelect}
onIssuePopupToggle={this.props.onIssuePopupToggle}
+ openIssuesByLine={openIssuesByLine}
selectedIssue={selectedIssue}
/>
</Line>
@@ -209,7 +268,16 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
};
render() {
- const { issues = [], sources } = this.props;
+ const { decoratedLinesMap } = this.state;
+
+ const {
+ hasSourcesAfter,
+ hasSourcesBefore,
+ issues = [],
+ loadingSourcesAfter,
+ loadingSourcesBefore,
+ sources,
+ } = this.props;
const displayCoverage = sources.some((s) => s.coverageStatus != null);
const displayDuplications = sources.some((s) => !!s.duplicated);
@@ -220,9 +288,9 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
return (
<SonarCodeColorizer>
<div className="it__source-viewer-code">
- {this.props.hasSourcesBefore && (
+ {hasSourcesBefore && (
<div className="source-viewer-more-code">
- {this.props.loadingSourcesBefore ? (
+ {loadingSourcesBefore ? (
<div className="js-component-viewer-loading-before">
<i className="spinner" />
<span className="note spacer-left">
@@ -244,27 +312,27 @@ export default class SourceViewerCode extends React.PureComponent<Props> {
<tbody>
{hasFileIssues &&
this.renderLine({
- line: ZERO_LINE,
- index: -1,
displayCoverage,
displayDuplications,
displayIssues,
+ index: -1,
+ line: ZERO_LINE,
})}
{sources.map((line, index) =>
this.renderLine({
- line,
- index,
displayCoverage,
displayDuplications,
displayIssues,
+ index,
+ line: decoratedLinesMap[line.line] || line,
})
)}
</tbody>
</table>
- {this.props.hasSourcesAfter && (
+ {hasSourcesAfter && (
<div className="source-viewer-more-code">
- {this.props.loadingSourcesAfter ? (
+ {loadingSourcesAfter ? (
<div className="js-component-viewer-loading-after">
<i className="spinner" />
<span className="note spacer-left">
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
index 731a881f3a2..3f11fccd9d1 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
@@ -172,7 +172,7 @@ it('should be able to interact with issue action', async () => {
).toBeInTheDocument();
});
-it('should load line when looking arround unloaded line', async () => {
+it('should load line when looking around unloaded line', async () => {
const rerender = renderSourceViewer({
aroundLine: 50,
component: componentsHandler.getHugeFileKey(),
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 bfd4403d196..9a725c5523a 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
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import classNames from 'classnames';
import { LineCoverage, LineMeta, LineNumber, LineWrapper } from 'design-system';
import { times } from 'lodash';
@@ -75,7 +76,7 @@ export default function Line(props: LineProps) {
displayAllIssues,
displayCoverage,
displayDuplications,
- displayLineNumberOptions,
+ displayLineNumberOptions = true,
displayLocationMarkers,
highlightedLocationMessage,
displayNewCodeUnderline,
@@ -125,9 +126,6 @@ export default function Line(props: LineProps) {
[line.line, onLineMouseLeave]
);
- // default is true
- const displayOptions = displayLineNumberOptions !== false;
-
const { branchLike, file } = useSourceViewerContext();
const permalink = getPathUrlAsString(
getCodeUrl(file.project, branchLike, file.key, line.line),
@@ -171,7 +169,7 @@ export default function Line(props: LineProps) {
className={classNames('it__source-line', { 'it__source-line-filtered': line.isNew })}
>
<LineNumber
- displayOptions={displayOptions}
+ displayOptions={displayLineNumberOptions}
firstLineNumber={firstLineNumber}
lineNumber={line.line}
ariaLabel={translateWithParameters('source_viewer.line_X', line.line)}
@@ -179,6 +177,7 @@ export default function Line(props: LineProps) {
/>
{displaySCM && <LineSCM line={line} previousLine={previousLine} />}
+
{displayIssues && !displayAllIssues ? (
<LineIssuesIndicator
issues={issues}
@@ -224,6 +223,7 @@ export default function Line(props: LineProps) {
coverageStatus={line.coverageStatus}
/>
)}
+
<LineCode
displayCoverageUnderline={displayCoverage && displayCoverageUnderline}
displayLocationMarkers={displayLocationMarkers}
diff --git a/server/sonar-web/src/main/js/helpers/code-viewer.ts b/server/sonar-web/src/main/js/helpers/code-viewer.ts
new file mode 100644
index 00000000000..1a4424fad2a
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/code-viewer.ts
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 type { LineMap, SourceLine } from '../types/types';
+
+export function decorateWithUnderlineFlags(line: SourceLine, sourcesMap: LineMap) {
+ const previousLine: SourceLine | undefined = sourcesMap[line.line - 1];
+
+ const decoratedLine = { ...line };
+
+ if (line.coverageStatus) {
+ decoratedLine.coverageBlock =
+ line.coverageStatus === previousLine?.coverageStatus
+ ? previousLine.coverageBlock ?? line.line
+ : line.line;
+ }
+
+ if (line.isNew) {
+ decoratedLine.newCodeBlock = previousLine?.isNew
+ ? previousLine.newCodeBlock ?? line.line
+ : line.line;
+ }
+
+ return decoratedLine;
+}