aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx268
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/utils.ts39
5 files changed, 162 insertions, 163 deletions
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 93e16726fbc..f6029dfa639 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
@@ -62,7 +62,6 @@ interface Props {
isLastOccurenceOfPrimaryComponent: boolean;
issue: TypeIssue;
issuesByLine: IssuesByLine;
- lastSnippetGroup: boolean;
loadDuplications: (component: string, line: SourceLine) => void;
locations: FlowLocation[];
onIssueSelect: (issueKey: string) => void;
@@ -256,8 +255,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
};
render() {
- const { branchLike, isLastOccurenceOfPrimaryComponent, issue, lastSnippetGroup, snippetGroup } =
- this.props;
+ const { branchLike, isLastOccurenceOfPrimaryComponent, issue, snippetGroup } = this.props;
const { additionalLines, loading, snippets } = this.state;
const snippetLines = linesForSnippets(snippets, {
@@ -320,7 +318,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
</IssueSourceViewerScrollContext.Consumer>
)}
- {snippetLines.map((snippet, index) => (
+ {snippetLines.map(({ snippet, sourcesMap }, index) => (
<SnippetViewer
key={snippets[index].index}
renderAdditionalChildInLine={this.renderIssuesList}
@@ -332,8 +330,6 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
highlightedLocationMessage={this.props.highlightedLocationMessage}
highlightedSymbols={this.state.highlightedSymbols}
index={snippets[index].index}
- issue={this.props.issue}
- lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1}
loadDuplications={this.loadDuplications}
locations={this.props.locations}
locationsByLine={getLocationsByLine(
@@ -345,6 +341,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
renderDuplicationPopup={this.renderDuplicationPopup}
snippet={snippet}
className={classNames({ 'sw-mt-2': index !== 0 })}
+ snippetSourcesMap={sourcesMap}
/>
))}
</>
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
index d1ec05f8ba0..8e5e8551a02 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx
@@ -21,6 +21,7 @@ import { findLastIndex, keyBy } from 'lodash';
import * as React from 'react';
import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
import { getIssueFlowSnippets } from '../../../api/issues';
+import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
import {
filterDuplicationBlocksByLine,
@@ -31,7 +32,6 @@ import {
duplicationsByLine as getDuplicationsByLine,
issuesByComponentAndLine,
} from '../../../components/SourceViewer/helpers/indexing';
-import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { WorkspaceContext } from '../../../components/workspace/context';
@@ -175,10 +175,11 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop
<DuplicationPopup
blocks={filterDuplicationBlocksByLine(blocks, line)}
branchLike={this.props.branchLike}
- duplicatedFiles={duplicatedFiles}
inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
+ duplicatedFiles={duplicatedFiles}
openComponent={openComponent}
sourceViewerFile={component}
+ duplicationHeader={translate('component_viewer.transition.duplication')}
/>
)}
</WorkspaceContext.Consumer>
@@ -234,7 +235,6 @@ export default class CrossComponentSourceViewer extends React.PureComponent<Prop
issue={issue}
issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
- lastSnippetGroup={i === locationsByComponent.length - 1}
loadDuplications={this.fetchDuplications}
locations={snippetGroup.locations || []}
onIssueSelect={this.props.onIssueSelect}
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
index 7a0becf21bb..b428953ca11 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
@@ -20,13 +20,15 @@
import classNames from 'classnames';
import {
CodeViewerExpander,
+ SonarCodeColorizer,
ThemeProp,
UnfoldDownIcon,
UnfoldUpIcon,
themeColor,
withTheme,
} from 'design-system';
-import * as React from 'react';
+import { debounce, throttle } from 'lodash';
+import React from 'react';
import Line from '../../../components/SourceViewer/components/Line';
import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing';
import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
@@ -39,13 +41,11 @@ import {
Duplication,
ExpandDirection,
FlowLocation,
- Issue,
+ LineMap,
LinearIssueLocation,
SourceLine,
SourceViewerFile,
} from '../../../types/types';
-import './SnippetViewer.css';
-import { LINES_BELOW_ISSUE } from './utils';
export interface SnippetViewerProps {
component: SourceViewerFile;
@@ -58,8 +58,6 @@ export interface SnippetViewerProps {
highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
highlightedSymbols: string[];
index: number;
- issue: Pick<Issue, 'key' | 'textRange' | 'line'>;
- lastSnippetOfLastGroup: boolean;
loadDuplications?: (line: SourceLine) => void;
locations: FlowLocation[];
locationsByLine: { [line: number]: LinearIssueLocation[] };
@@ -68,154 +66,142 @@ export interface SnippetViewerProps {
renderDuplicationPopup: (index: number, line: number) => React.ReactNode;
snippet: SourceLine[];
className?: string;
+ snippetSourcesMap?: LineMap;
}
-class SnippetViewer extends React.PureComponent<SnippetViewerProps & ThemeProp> {
- expandBlock = (direction: ExpandDirection) => () =>
- this.props.expandBlock(this.props.index, direction);
+type Props = SnippetViewerProps & ThemeProp;
- renderLine({
- displayDuplications,
- displaySCM,
- index,
- issueLocations,
- line,
- snippet,
- symbols,
- verticalBuffer,
- }: {
- displayDuplications: boolean;
- displaySCM?: boolean;
- index: number;
- issueLocations: LinearIssueLocation[];
- line: SourceLine;
- snippet: SourceLine[];
- symbols: string[];
- verticalBuffer: number;
- }) {
- const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
+function SnippetViewer(props: Props) {
+ const expandBlock = (direction: ExpandDirection) => () => {
+ props.expandBlock(props.index, direction);
+ };
- const { displayLineNumberOptions, duplications, duplicationsByLine } = this.props;
- const duplicationsCount = duplications ? duplications.length : 0;
- const lineDuplications =
- (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
+ const { component, displaySCM, locationsByLine, snippet, theme, className } = props;
- const firstLineNumber = snippet && snippet.length ? snippet[0].line : 0;
- const noop = () => {};
+ const { displayLineNumberOptions, duplications, duplicationsByLine, snippetSourcesMap } = props;
+ const duplicationsCount = duplications ? duplications.length : 0;
- return (
- <Line
- displayCoverage={true}
- displayDuplications={displayDuplications}
- displayIssues={false}
- displayLineNumberOptions={displayLineNumberOptions}
- displayLocationMarkers={true}
- displaySCM={displaySCM}
- duplications={lineDuplications}
- duplicationsCount={duplicationsCount}
- firstLineNumber={firstLineNumber}
- highlighted={false}
- highlightedLocationMessage={optimizeLocationMessage(
- this.props.highlightedLocationMessage,
- secondaryIssueLocations
- )}
- highlightedSymbols={optimizeHighlightedSymbols(symbols, this.props.highlightedSymbols)}
- issueLocations={issueLocations}
- issues={[]}
- key={line.line}
- last={false}
- line={line}
- loadDuplications={this.props.loadDuplications || noop}
- onIssueSelect={noop}
- onIssueUnselect={noop}
- onIssuesClose={noop}
- onIssuesOpen={noop}
- onLocationSelect={this.props.onLocationSelect}
- onSymbolClick={this.props.handleSymbolClick}
- openIssues={false}
- previousLine={index > 0 ? snippet[index - 1] : undefined}
- renderDuplicationPopup={this.props.renderDuplicationPopup}
- secondaryIssueLocations={secondaryIssueLocations}
- verticalBuffer={verticalBuffer}
- >
- {this.props.renderAdditionalChildInLine && this.props.renderAdditionalChildInLine(line)}
- </Line>
- );
- }
+ const firstLineNumber = snippet?.length ? snippet[0].line : 0;
+ const noop = () => {
+ /* noop */
+ };
+ const lastLine = component.measures?.lines && parseInt(component.measures.lines, 10);
+
+ const symbols = symbolsByLine(snippet);
- render() {
- const {
- component,
- displaySCM,
- issue,
- lastSnippetOfLastGroup,
- locationsByLine,
- snippet,
- theme,
- className,
- } = this.props;
- const lastLine =
- component.measures && component.measures.lines && parseInt(component.measures.lines, 10);
+ const displayDuplications =
+ Boolean(props.loadDuplications) && snippet.some((s) => !!s.duplicated);
- const symbols = symbolsByLine(snippet);
+ const borderColor = themeColor('codeLineBorder')({ theme });
- const bottomLine = snippet[snippet.length - 1].line;
- const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
+ const THROTTLE_SHORT_DELAY = 10;
+ const [hoveredLine, setHoveredLine] = React.useState<SourceLine | undefined>();
- const verticalBuffer =
- lastSnippetOfLastGroup && issueLine
- ? Math.max(0, LINES_BELOW_ISSUE - (bottomLine - issueLine))
- : 0;
+ const onLineMouseEnter = React.useMemo(
+ () =>
+ throttle(
+ (hoveredLine: number) =>
+ snippetSourcesMap ? setHoveredLine(snippetSourcesMap[hoveredLine]) : undefined,
+ THROTTLE_SHORT_DELAY
+ ),
+ [snippetSourcesMap]
+ );
- const displayDuplications =
- Boolean(this.props.loadDuplications) && snippet.some((s) => !!s.duplicated);
+ const onLineMouseLeave = React.useMemo(
+ () =>
+ debounce(
+ (line: number) =>
+ setHoveredLine((hoveredLine) => (hoveredLine?.line === line ? undefined : hoveredLine)),
+ THROTTLE_SHORT_DELAY
+ ),
+ []
+ );
- const borderColor = themeColor('codeLineBorder')({ theme });
+ return (
+ <div
+ className={classNames('it__source-viewer-code', className)}
+ style={{ border: `1px solid ${borderColor}` }}
+ >
+ <SonarCodeColorizer>
+ {snippet[0].line > 1 && (
+ <CodeViewerExpander
+ direction="UP"
+ className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2"
+ onClick={expandBlock('up')}
+ >
+ <UnfoldUpIcon aria-label={translate('source_viewer.expand_above')} />
+ </CodeViewerExpander>
+ )}
+ <table className="sw-w-full">
+ <tbody>
+ {snippet.map((line, index) => {
+ const secondaryIssueLocations = getSecondaryIssueLocationsForLine(
+ line,
+ props.locations
+ );
+ const lineDuplications =
+ (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
- return (
- <div
- className={classNames('source-viewer-code', className)}
- style={{ border: `1px solid ${borderColor}` }}
- >
- <div>
- {snippet[0].line > 1 && (
- <CodeViewerExpander
- direction="UP"
- className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2"
- onClick={this.expandBlock('up')}
- >
- <UnfoldUpIcon aria-label={translate('source_viewer.expand_above')} />
- </CodeViewerExpander>
- )}
- <table>
- <tbody>
- {snippet.map((line, index) =>
- this.renderLine({
- displayDuplications,
- displaySCM,
- index,
- issueLocations: locationsByLine[line.line] || [],
- line,
- snippet,
- symbols: symbols[line.line],
- verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0,
- })
- )}
- </tbody>
- </table>
- {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
- <CodeViewerExpander
- className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2"
- onClick={this.expandBlock('down')}
- direction="DOWN"
- >
- <UnfoldDownIcon aria-label={translate('source_viewer.expand_below')} />
- </CodeViewerExpander>
- )}
- </div>
- </div>
- );
- }
+ const displayCoverageUnderline = hoveredLine?.coverageBlock === line.coverageBlock;
+ const displayNewCodeUnderline = hoveredLine?.newCodeBlock === line.line;
+ return (
+ <Line
+ displayCoverage={true}
+ displayCoverageUnderline={displayCoverageUnderline}
+ displayNewCodeUnderline={displayNewCodeUnderline}
+ displayDuplications={displayDuplications}
+ displayIssues={false}
+ displayLineNumberOptions={displayLineNumberOptions}
+ displayLocationMarkers={true}
+ displaySCM={displaySCM}
+ duplications={lineDuplications}
+ duplicationsCount={duplicationsCount}
+ firstLineNumber={firstLineNumber}
+ highlighted={false}
+ highlightedLocationMessage={optimizeLocationMessage(
+ props.highlightedLocationMessage,
+ secondaryIssueLocations
+ )}
+ highlightedSymbols={optimizeHighlightedSymbols(
+ symbols[line.line],
+ props.highlightedSymbols
+ )}
+ issueLocations={locationsByLine[line.line] || []}
+ issues={[]}
+ key={line.line}
+ line={line}
+ loadDuplications={props.loadDuplications ?? noop}
+ onIssueSelect={noop}
+ onIssueUnselect={noop}
+ onIssuesClose={noop}
+ onIssuesOpen={noop}
+ onLocationSelect={props.onLocationSelect}
+ onSymbolClick={props.handleSymbolClick}
+ openIssues={false}
+ previousLine={index > 0 ? snippet[index - 1] : undefined}
+ renderDuplicationPopup={props.renderDuplicationPopup}
+ secondaryIssueLocations={secondaryIssueLocations}
+ onLineMouseEnter={onLineMouseEnter}
+ onLineMouseLeave={onLineMouseLeave}
+ >
+ {props.renderAdditionalChildInLine?.(line)}
+ </Line>
+ );
+ })}
+ </tbody>
+ </table>
+ {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
+ <CodeViewerExpander
+ className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2"
+ onClick={expandBlock('down')}
+ direction="DOWN"
+ >
+ <UnfoldDownIcon aria-label={translate('source_viewer.expand_below')} />
+ </CodeViewerExpander>
+ )}
+ </SonarCodeColorizer>
+ </div>
+ );
}
export default withTheme(SnippetViewer);
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
index cdbcdb84b88..df138d37b67 100644
--- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx
@@ -22,7 +22,6 @@ import { range } from 'lodash';
import * as React from 'react';
import { byRole } from 'testing-library-selector';
import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/mocks/sources';
-import { mockIssue } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import SnippetViewer, { SnippetViewerProps } from '../SnippetViewer';
@@ -102,8 +101,6 @@ function renderSnippetViewer(props: Partial<SnippetViewerProps> = {}) {
highlightedLocationMessage={{ index: 0, text: '' }}
highlightedSymbols={[]}
index={0}
- issue={mockIssue()}
- lastSnippetOfLastGroup={false}
loadDuplications={jest.fn()}
locations={[]}
locationsByLine={{}}
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 10287fb3267..f84d5db25b0 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,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 { isDefined } from '../../../helpers/types';
import { ComponentQualifier } from '../../../types/component';
import {
ExpandDirection,
@@ -134,18 +135,36 @@ 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
- .map((snippet) => {
- const lines = [];
- for (let i = snippet.start; i <= snippet.end; i++) {
- if (componentLines[i]) {
- lines.push(componentLines[i]);
- }
+ return snippets.reduce<Array<{ snippet: SourceLine[]; sourcesMap: LineMap }>>((acc, snippet) => {
+ const snippetSources = [];
+ const snippetSourcesMap: LineMap = {};
+ for (let idx = snippet.start; idx <= snippet.end; idx++) {
+ if (isDefined(componentLines[idx])) {
+ const line = decorateWithUnderlineFlags(componentLines[idx], snippetSourcesMap);
+ snippetSourcesMap[line.line] = line;
+ snippetSources.push(line);
}
- return lines;
- })
- .filter((snippet) => snippet.length > 0);
+ }
+
+ if (snippetSources.length > 0) {
+ acc.push({ snippet: snippetSources, sourcesMap: snippetSourcesMap });
+ }
+ return acc;
+ }, []);
}
export function groupLocationsByComponent(