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 { getSources } from '../../../api/components';
22 import IssueMessageBox from '../../../components/issue/IssueMessageBox';
23 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
24 import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
25 import { getBranchLikeQuery } from '../../../helpers/branch-like';
26 import { BranchLike } from '../../../types/branch-like';
27 import { isFile } from '../../../types/component';
39 } from '../../../types/types';
40 import IssueSourceViewerHeader from './IssueSourceViewerHeader';
41 import SnippetViewer from './SnippetViewer';
52 branchLike: BranchLike | undefined;
53 duplications?: Duplication[];
54 duplicationsByLine?: { [line: number]: number[] };
55 highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
56 isLastOccurenceOfPrimaryComponent: boolean;
58 issuesByLine: IssuesByLine;
59 lastSnippetGroup: boolean;
60 loadDuplications: (component: string, line: SourceLine) => void;
61 locations: FlowLocation[];
62 onIssueSelect: (issueKey: string) => void;
63 onLocationSelect: (index: number) => void;
64 renderDuplicationPopup: (
65 component: SourceViewerFile,
69 snippetGroup: SnippetGroup;
73 additionalLines: { [line: number]: SourceLine };
74 highlightedSymbols: string[];
79 export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> {
82 constructor(props: Props) {
86 highlightedSymbols: [],
94 this.createSnippetsFromProps();
97 componentWillUnmount() {
101 createSnippetsFromProps() {
102 const { issue, snippetGroup } = this.props;
104 const snippets = createSnippets({
105 component: snippetGroup.component.key,
108 snippetGroup.locations.length === 0 ? [getPrimaryLocation(issue)] : snippetGroup.locations
111 this.setState({ snippets });
114 expandBlock = (snippetIndex: number, direction: ExpandDirection): Promise<void> => {
115 const { branchLike, snippetGroup } = this.props;
116 const { key } = snippetGroup.component;
117 const { snippets } = this.state;
118 const snippet = snippets.find(s => s.index === snippetIndex);
120 return Promise.reject();
122 // Extend by EXPAND_BY_LINES and add buffer for merging snippets
123 const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
127 from: Math.max(1, snippet.start - extension),
128 to: snippet.start - 1
131 from: snippet.end + 1,
132 to: snippet.end + extension
137 ...getBranchLikeQuery(branchLike)
140 lines.reduce((lineMap: Dict<SourceLine>, line) => {
141 line.coverageStatus = getCoverageStatus(line);
142 lineMap[line.line] = line;
146 .then(newLinesMapped => {
147 const newSnippets = expandSnippet({
153 this.setState(({ additionalLines }) => {
154 const combinedLines = { ...additionalLines, ...newLinesMapped };
156 additionalLines: combinedLines,
157 snippets: newSnippets.filter(s => !s.toDelete)
163 expandComponent = () => {
164 const { branchLike, snippetGroup } = this.props;
165 const { key } = snippetGroup.component;
167 this.setState({ loading: true });
169 getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
172 this.setState(({ additionalLines }) => {
173 const combinedLines = { ...additionalLines, ...lines };
175 additionalLines: combinedLines,
177 snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }]
184 this.setState({ loading: false });
190 handleSymbolClick = (clickedSymbols: string[]) => {
191 this.setState(({ highlightedSymbols }) => {
192 const newHighlightedSymbols = clickedSymbols.filter(
193 symb => !highlightedSymbols.includes(symb)
195 return { highlightedSymbols: newHighlightedSymbols };
199 loadDuplications = (line: SourceLine) => {
200 this.props.loadDuplications(this.props.snippetGroup.component.key, line);
203 renderDuplicationPopup = (index: number, line: number) => {
204 return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
207 renderIssuesList = (line: SourceLine) => {
208 const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
210 issue.component === snippetGroup.component.key && issue.textRange !== undefined
211 ? locationsByLine([issue])
214 const isFlow = issue.secondaryLocations.length === 0;
215 const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
216 const issuesForLine = issuesByLine[line.line] || [];
217 const issueLocations = includeIssueLocation ? locations[line.line] : [];
220 issuesForLine.length > 0 && (
222 {issuesForLine.map(issueToDisplay => (
224 selected={!!(issueToDisplay.key === issue.key && issueLocations.length > 0)}
225 key={issueToDisplay.key}
226 issue={issueToDisplay}
227 onClick={this.props.onIssueSelect}
238 isLastOccurenceOfPrimaryComponent,
243 const { additionalLines, loading, snippets } = this.state;
245 issue.component === snippetGroup.component.key && issue.textRange !== undefined
246 ? locationsByLine([issue])
250 snippets.length === 1 &&
251 snippetGroup.component.measures &&
252 snippets[0].end - snippets[0].start ===
253 parseInt(snippetGroup.component.measures.lines || '', 10);
255 const snippetLines = linesForSnippets(snippets, {
256 ...snippetGroup.sources,
260 const isFlow = issue.secondaryLocations.length === 0;
261 const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
265 <IssueSourceViewerHeader
266 branchLike={branchLike}
267 expandable={!fullyShown && isFile(snippetGroup.component.q)}
269 onExpand={this.expandComponent}
270 sourceViewerFile={snippetGroup.component}
273 {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
274 <IssueMessageBox selected={true} issue={issue} onClick={this.props.onIssueSelect} />
276 {snippetLines.map((snippet, index) => (
278 key={snippets[index].index}
279 renderAdditionalChildInLine={this.renderIssuesList}
280 component={this.props.snippetGroup.component}
281 duplications={this.props.duplications}
282 duplicationsByLine={this.props.duplicationsByLine}
283 expandBlock={this.expandBlock}
284 handleSymbolClick={this.handleSymbolClick}
285 highlightedLocationMessage={this.props.highlightedLocationMessage}
286 highlightedSymbols={this.state.highlightedSymbols}
288 issue={this.props.issue}
289 lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1}
290 loadDuplications={this.loadDuplications}
291 locations={this.props.locations}
292 locationsByLine={includeIssueLocation ? locations : {}}
293 onLocationSelect={this.props.onLocationSelect}
294 renderDuplicationPopup={this.renderDuplicationPopup}