3 * Copyright (C) 2009-2019 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 import * as React from 'react';
21 import * as classNames from 'classnames';
30 import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
31 import Line from '../../../components/SourceViewer/components/Line';
32 import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
33 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
34 import { getSources } from '../../../api/components';
35 import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
36 import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
38 optimizeLocationMessage,
39 optimizeHighlightedSymbols,
41 } from '../../../components/SourceViewer/helpers/lines';
42 import { translate } from '../../../helpers/l10n';
45 branchLike: T.BranchLike | undefined;
46 duplications?: T.Duplication[];
47 duplicationsByLine?: { [line: number]: number[] };
48 highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
50 issuePopup?: { issue: string; name: string };
51 issuesByLine: T.IssuesByLine;
53 linePopup?: T.LinePopup;
54 loadDuplications: (component: string, line: T.SourceLine) => void;
55 locations: T.FlowLocation[];
56 onIssueChange: (issue: T.Issue) => void;
57 onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
58 onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void;
59 onLocationSelect: (index: number) => void;
60 renderDuplicationPopup: (
61 component: T.SourceViewerFile,
65 scroll?: (element: HTMLElement) => void;
66 snippetGroup: T.SnippetGroup;
70 additionalLines: { [line: number]: T.SourceLine };
71 highlightedSymbols: string[];
73 openIssuesByLine: T.Dict<boolean>;
74 snippets: T.SourceLine[][];
77 export default class ComponentSourceSnippetViewer extends React.PureComponent<Props, State> {
81 highlightedSymbols: [],
89 this.createSnippetsFromProps();
92 componentWillUnmount() {
96 createSnippetsFromProps() {
97 const mainLocation: T.FlowLocation = {
98 component: this.props.issue.component,
99 textRange: this.props.issue.textRange || {
106 const snippets = createSnippets(
107 this.props.snippetGroup.locations.concat(mainLocation),
108 this.props.snippetGroup.sources,
111 this.setState({ snippets });
114 expandBlock = (snippetIndex: number, direction: T.ExpandDirection) => {
115 const { snippets } = this.state;
117 const snippet = snippets[snippetIndex];
119 // Extend by EXPAND_BY_LINES and add buffer for merging snippets
120 const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
125 from: Math.max(1, snippet[0].line - extension),
126 to: snippet[0].line - 1
129 from: snippet[snippet.length - 1].line + 1,
130 to: snippet[snippet.length - 1].line + extension
134 key: this.props.snippetGroup.component.key,
138 lines.reduce((lineMap: T.Dict<T.SourceLine>, line) => {
139 line.coverageStatus = getCoverageStatus(line);
140 lineMap[line.line] = line;
147 this.setState(({ additionalLines, snippets }) => {
148 const combinedLines = { ...additionalLines, ...newLinesMapped };
151 additionalLines: combinedLines,
152 snippets: expandSnippet({
154 lines: { ...combinedLines, ...this.props.snippetGroup.sources },
166 expandComponent = () => {
167 const { key } = this.props.snippetGroup.component;
169 this.setState({ loading: true });
171 getSources({ key }).then(
174 this.setState({ loading: false, snippets: [lines] });
179 this.setState({ loading: false });
185 handleLinePopupToggle = (linePopup: T.LinePopup) => {
186 this.props.onLinePopupToggle({
188 component: this.props.snippetGroup.component.key
192 handleOpenIssues = (line: T.SourceLine) => {
193 this.setState(state => ({
194 openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
198 handleCloseIssues = (line: T.SourceLine) => {
199 this.setState(state => ({
200 openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
204 handleSymbolClick = (highlightedSymbols: string[]) => {
205 this.setState({ highlightedSymbols });
208 loadDuplications = (line: T.SourceLine) => {
209 this.props.loadDuplications(this.props.snippetGroup.component.key, line);
212 renderDuplicationPopup = (index: number, line: number) => {
213 return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
226 displayDuplications: boolean;
228 issuesForLine: T.Issue[];
229 issueLocations: T.LinearIssueLocation[];
231 snippet: T.SourceLine[];
233 verticalBuffer: number;
235 const { openIssuesByLine } = this.state;
236 const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
238 const { duplications, duplicationsByLine } = this.props;
239 const duplicationsCount = duplications ? duplications.length : 0;
240 const lineDuplications =
241 (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
243 const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
247 branchLike={undefined}
248 displayAllIssues={false}
249 displayCoverage={true}
250 displayDuplications={displayDuplications}
251 displayIssues={!isSinkLine || issuesForLine.length > 1}
252 displayLocationMarkers={true}
253 duplications={lineDuplications}
254 duplicationsCount={duplicationsCount}
256 highlightedLocationMessage={optimizeLocationMessage(
257 this.props.highlightedLocationMessage,
258 secondaryIssueLocations
260 highlightedSymbols={optimizeHighlightedSymbols(symbols, this.state.highlightedSymbols)}
261 issueLocations={issueLocations}
262 issuePopup={this.props.issuePopup}
263 issues={issuesForLine}
267 linePopup={this.props.linePopup}
268 loadDuplications={this.loadDuplications}
269 onIssueChange={this.props.onIssueChange}
270 onIssuePopupToggle={this.props.onIssuePopupToggle}
271 onIssueSelect={() => {}}
272 onIssueUnselect={() => {}}
273 onIssuesClose={this.handleCloseIssues}
274 onIssuesOpen={this.handleOpenIssues}
275 onLinePopupToggle={this.handleLinePopupToggle}
276 onLocationSelect={this.props.onLocationSelect}
277 onSymbolClick={this.handleSymbolClick}
278 openIssues={openIssuesByLine[line.line]}
279 previousLine={index > 0 ? snippet[index - 1] : undefined}
280 renderDuplicationPopup={this.renderDuplicationPopup}
281 scroll={this.props.scroll}
282 secondaryIssueLocations={secondaryIssueLocations}
283 selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
284 verticalBuffer={verticalBuffer}
297 snippet: T.SourceLine[];
300 issuesByLine: T.IssuesByLine;
301 locationsByLine: { [line: number]: T.LinearIssueLocation[] };
304 const { component } = this.props.snippetGroup;
306 component.measures && component.measures.lines && parseInt(component.measures.lines, 10);
308 const symbols = symbolsByLine(snippet);
310 const expandBlock = (direction: T.ExpandDirection) => () => this.expandBlock(index, direction);
312 const bottomLine = snippet[snippet.length - 1].line;
313 const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
314 const lowestVisibleIssue = Math.max(
315 ...Object.keys(issuesByLine)
316 .map(k => parseInt(k, 10))
317 .filter(l => inSnippet(l, snippet) && (l === issueLine || this.state.openIssuesByLine[l]))
319 const verticalBuffer = last
320 ? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
323 const displayDuplications = snippet.some(s => !!s.duplicated);
326 <div className="source-viewer-code snippet" key={index}>
327 {snippet[0].line > 1 && (
329 aria-label={translate('source_viewer.expand_above')}
330 className="expand-block expand-block-above"
331 onClick={expandBlock('up')}
333 <ExpandSnippetIcon />
336 <table className="source-table">
338 {snippet.map((line, index) =>
342 issuesForLine: issuesByLine[line.line] || [],
343 issueLocations: locationsByLine[line.line] || [],
346 symbols: symbols[line.line],
347 verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
352 {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
354 aria-label={translate('source_viewer.expand_below')}
355 className="expand-block expand-block-below"
356 onClick={expandBlock('down')}
358 <ExpandSnippetIcon />
366 const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props;
367 const { loading, snippets } = this.state;
368 const locations = locationsByLine([issue]);
371 snippets.length === 1 &&
372 snippetGroup.component.measures &&
373 snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10);
377 className={classNames('component-source-container', {
378 'source-duplications-expanded': duplications && duplications.length > 0
380 <SourceViewerHeaderSlim
381 branchLike={branchLike}
382 expandable={!fullyShown}
384 onExpand={this.expandComponent}
385 sourceViewerFile={snippetGroup.component}
387 {snippets.map((snippet, index) =>
392 issuesByLine: last ? issuesByLine : {},
393 locationsByLine: last && index === snippets.length - 1 ? locations : {},
394 last: last && index === snippets.length - 1