3 * Copyright (C) 2009-2022 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 { FormattedMessage } from 'react-intl';
22 import { getSources } from '../../../api/components';
23 import IssueMessageBox from '../../../components/issue/IssueMessageBox';
24 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
25 import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
26 import { Alert } from '../../../components/ui/Alert';
27 import { getBranchLikeQuery } from '../../../helpers/branch-like';
28 import { translate } from '../../../helpers/l10n';
29 import { BranchLike } from '../../../types/branch-like';
30 import { ComponentQualifier, isFile } from '../../../types/component';
31 import { IssueStatus } from '../../../types/issues';
43 } from '../../../types/types';
44 import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext';
45 import IssueSourceViewerHeader from './IssueSourceViewerHeader';
46 import SnippetViewer from './SnippetViewer';
57 branchLike: BranchLike | undefined;
58 duplications?: Duplication[];
59 duplicationsByLine?: { [line: number]: number[] };
60 highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
61 isLastOccurenceOfPrimaryComponent: boolean;
63 issuesByLine: IssuesByLine;
64 lastSnippetGroup: boolean;
65 loadDuplications: (component: string, line: SourceLine) => void;
66 locations: FlowLocation[];
67 onIssueSelect: (issueKey: string) => void;
68 onLocationSelect: (index: number) => void;
69 renderDuplicationPopup: (
70 component: SourceViewerFile,
74 snippetGroup: SnippetGroup;
78 additionalLines: { [line: number]: SourceLine };
79 highlightedSymbols: string[];
84 export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> {
87 constructor(props: Props) {
91 highlightedSymbols: [],
99 this.createSnippetsFromProps();
102 componentWillUnmount() {
103 this.mounted = false;
106 createSnippetsFromProps() {
107 const { issue, snippetGroup } = this.props;
109 const snippets = createSnippets({
110 component: snippetGroup.component.key,
113 snippetGroup.locations.length === 0
114 ? [getPrimaryLocation(issue)]
115 : [getPrimaryLocation(issue), ...snippetGroup.locations],
118 this.setState({ snippets });
121 expandBlock = (snippetIndex: number, direction: ExpandDirection): Promise<void> => {
122 const { branchLike, snippetGroup } = this.props;
123 const { key } = snippetGroup.component;
124 const { snippets } = this.state;
125 const snippet = snippets.find((s) => s.index === snippetIndex);
127 return Promise.reject();
129 // Extend by EXPAND_BY_LINES and add buffer for merging snippets
130 const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
134 from: Math.max(1, snippet.start - extension),
135 to: snippet.start - 1,
138 from: snippet.end + 1,
139 to: snippet.end + extension,
144 ...getBranchLikeQuery(branchLike),
147 lines.reduce((lineMap: Dict<SourceLine>, line) => {
148 line.coverageStatus = getCoverageStatus(line);
149 lineMap[line.line] = line;
153 .then((newLinesMapped) => {
154 const newSnippets = expandSnippet({
160 this.setState(({ additionalLines }) => {
161 const combinedLines = { ...additionalLines, ...newLinesMapped };
163 additionalLines: combinedLines,
164 snippets: newSnippets.filter((s) => !s.toDelete),
170 expandComponent = () => {
171 const { branchLike, snippetGroup } = this.props;
172 const { key } = snippetGroup.component;
174 this.setState({ loading: true });
176 getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
179 this.setState(({ additionalLines }) => {
180 const combinedLines = { ...additionalLines, ...lines };
182 additionalLines: combinedLines,
184 snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }],
191 this.setState({ loading: false });
197 handleSymbolClick = (clickedSymbols: string[]) => {
198 this.setState(({ highlightedSymbols }) => {
199 const newHighlightedSymbols = clickedSymbols.filter(
200 (symb) => !highlightedSymbols.includes(symb)
202 return { highlightedSymbols: newHighlightedSymbols };
206 loadDuplications = (line: SourceLine) => {
207 this.props.loadDuplications(this.props.snippetGroup.component.key, line);
210 renderDuplicationPopup = (index: number, line: number) => {
211 return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
214 renderIssuesList = (line: SourceLine) => {
215 const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
217 issue.component === snippetGroup.component.key && issue.textRange !== undefined
218 ? locationsByLine([issue])
221 const isFlow = issue.secondaryLocations.length === 0;
222 const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
223 const issueLocations = includeIssueLocation ? locations[line.line] : [];
224 const issuesForLine = (issuesByLine[line.line] || []).filter(
226 issue.key !== issueForline.key ||
227 (issue.key === issueForline.key && issueLocations.length > 0)
231 issuesForLine.length > 0 && (
233 {issuesForLine.map((issueToDisplay) => {
234 const isSelectedIssue = issueToDisplay.key === issue.key;
236 <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
239 selected={isSelectedIssue}
240 issue={issueToDisplay}
241 onClick={this.props.onIssueSelect}
242 ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
245 </IssueSourceViewerScrollContext.Consumer>
254 const { branchLike, isLastOccurenceOfPrimaryComponent, issue, lastSnippetGroup, snippetGroup } =
256 const { additionalLines, loading, snippets } = this.state;
258 issue.component === snippetGroup.component.key && issue.textRange !== undefined
259 ? locationsByLine([issue])
263 snippets.length === 1 &&
264 snippetGroup.component.measures &&
265 snippets[0].end - snippets[0].start ===
266 parseInt(snippetGroup.component.measures.lines || '', 10);
268 const snippetLines = linesForSnippets(snippets, {
269 ...snippetGroup.sources,
273 const isFlow = issue.secondaryLocations.length === 0;
274 const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
276 const issueIsClosed = issue.status === IssueStatus.Closed;
277 const issueIsFileLevel = issue.componentQualifier === ComponentQualifier.File;
278 const closedIssueMessageKey = issueIsFileLevel
279 ? 'issue.closed.file_level'
280 : 'issue.closed.project_level';
285 <Alert variant="success">
287 id={closedIssueMessageKey}
288 defaultMessage={translate(closedIssueMessageKey)}
292 {translate('issue.status', issue.status)} (
293 {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
301 <IssueSourceViewerHeader
302 branchLike={branchLike}
303 className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''}
304 expandable={!fullyShown && isFile(snippetGroup.component.q)}
306 onExpand={this.expandComponent}
307 sourceViewerFile={snippetGroup.component}
310 {issue.component === snippetGroup.component.key &&
311 issue.textRange === undefined &&
313 <IssueSourceViewerScrollContext.Consumer>
318 onClick={this.props.onIssueSelect}
319 ref={ctx?.registerPrimaryLocationRef}
322 </IssueSourceViewerScrollContext.Consumer>
325 {snippetLines.map((snippet, index) => (
327 key={snippets[index].index}
328 renderAdditionalChildInLine={this.renderIssuesList}
329 component={this.props.snippetGroup.component}
330 duplications={this.props.duplications}
331 duplicationsByLine={this.props.duplicationsByLine}
332 expandBlock={this.expandBlock}
333 handleSymbolClick={this.handleSymbolClick}
334 highlightedLocationMessage={this.props.highlightedLocationMessage}
335 highlightedSymbols={this.state.highlightedSymbols}
336 index={snippets[index].index}
337 issue={this.props.issue}
338 lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1}
339 loadDuplications={this.loadDuplications}
340 locations={this.props.locations}
341 locationsByLine={includeIssueLocation ? locations : {}}
342 onLocationSelect={this.props.onLocationSelect}
343 renderDuplicationPopup={this.renderDuplicationPopup}