]> source.dussan.org Git - sonarqube.git/blob
d0ec15e8d2aebecea5e4eed403cc2280fdb75b44
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
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';
32 import {
33   Dict,
34   Duplication,
35   ExpandDirection,
36   FlowLocation,
37   Issue as TypeIssue,
38   IssuesByLine,
39   Snippet,
40   SnippetGroup,
41   SourceLine,
42   SourceViewerFile,
43 } from '../../../types/types';
44 import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext';
45 import IssueSourceViewerHeader from './IssueSourceViewerHeader';
46 import SnippetViewer from './SnippetViewer';
47 import {
48   createSnippets,
49   expandSnippet,
50   EXPAND_BY_LINES,
51   getPrimaryLocation,
52   linesForSnippets,
53   MERGE_DISTANCE,
54 } from './utils';
55
56 interface Props {
57   branchLike: BranchLike | undefined;
58   duplications?: Duplication[];
59   duplicationsByLine?: { [line: number]: number[] };
60   highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
61   isLastOccurenceOfPrimaryComponent: boolean;
62   issue: TypeIssue;
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,
71     index: number,
72     line: number
73   ) => React.ReactNode;
74   snippetGroup: SnippetGroup;
75 }
76
77 interface State {
78   additionalLines: { [line: number]: SourceLine };
79   highlightedSymbols: string[];
80   loading: boolean;
81   snippets: Snippet[];
82 }
83
84 export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> {
85   mounted = false;
86
87   constructor(props: Props) {
88     super(props);
89     this.state = {
90       additionalLines: {},
91       highlightedSymbols: [],
92       loading: false,
93       snippets: [],
94     };
95   }
96
97   componentDidMount() {
98     this.mounted = true;
99     this.createSnippetsFromProps();
100   }
101
102   componentWillUnmount() {
103     this.mounted = false;
104   }
105
106   createSnippetsFromProps() {
107     const { issue, snippetGroup } = this.props;
108
109     const snippets = createSnippets({
110       component: snippetGroup.component.key,
111       issue,
112       locations:
113         snippetGroup.locations.length === 0
114           ? [getPrimaryLocation(issue)]
115           : [getPrimaryLocation(issue), ...snippetGroup.locations],
116     });
117
118     this.setState({ snippets });
119   }
120
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);
126     if (!snippet) {
127       return Promise.reject();
128     }
129     // Extend by EXPAND_BY_LINES and add buffer for merging snippets
130     const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
131     const range =
132       direction === 'up'
133         ? {
134             from: Math.max(1, snippet.start - extension),
135             to: snippet.start - 1,
136           }
137         : {
138             from: snippet.end + 1,
139             to: snippet.end + extension,
140           };
141     return getSources({
142       key,
143       ...range,
144       ...getBranchLikeQuery(branchLike),
145     })
146       .then((lines) =>
147         lines.reduce((lineMap: Dict<SourceLine>, line) => {
148           line.coverageStatus = getCoverageStatus(line);
149           lineMap[line.line] = line;
150           return lineMap;
151         }, {})
152       )
153       .then((newLinesMapped) => {
154         const newSnippets = expandSnippet({
155           direction,
156           snippetIndex,
157           snippets,
158         });
159
160         this.setState(({ additionalLines }) => {
161           const combinedLines = { ...additionalLines, ...newLinesMapped };
162           return {
163             additionalLines: combinedLines,
164             snippets: newSnippets.filter((s) => !s.toDelete),
165           };
166         });
167       });
168   };
169
170   expandComponent = () => {
171     const { branchLike, snippetGroup } = this.props;
172     const { key } = snippetGroup.component;
173
174     this.setState({ loading: true });
175
176     getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
177       (lines) => {
178         if (this.mounted) {
179           this.setState(({ additionalLines }) => {
180             const combinedLines = { ...additionalLines, ...lines };
181             return {
182               additionalLines: combinedLines,
183               loading: false,
184               snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }],
185             };
186           });
187         }
188       },
189       () => {
190         if (this.mounted) {
191           this.setState({ loading: false });
192         }
193       }
194     );
195   };
196
197   handleSymbolClick = (clickedSymbols: string[]) => {
198     this.setState(({ highlightedSymbols }) => {
199       const newHighlightedSymbols = clickedSymbols.filter(
200         (symb) => !highlightedSymbols.includes(symb)
201       );
202       return { highlightedSymbols: newHighlightedSymbols };
203     });
204   };
205
206   loadDuplications = (line: SourceLine) => {
207     this.props.loadDuplications(this.props.snippetGroup.component.key, line);
208   };
209
210   renderDuplicationPopup = (index: number, line: number) => {
211     return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
212   };
213
214   renderIssuesList = (line: SourceLine) => {
215     const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
216     const locations =
217       issue.component === snippetGroup.component.key && issue.textRange !== undefined
218         ? locationsByLine([issue])
219         : {};
220
221     const isFlow = issue.secondaryLocations.length === 0;
222     const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
223     const issuesForLine = issuesByLine[line.line] || [];
224     const issueLocations = includeIssueLocation ? locations[line.line] : [];
225
226     return (
227       issuesForLine.length > 0 && (
228         <div>
229           {issuesForLine.map((issueToDisplay) => {
230             const isSelectedIssue = issueToDisplay.key === issue.key;
231             return (
232               <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
233                 {(ctx) => (
234                   <IssueMessageBox
235                     selected={!!(isSelectedIssue && issueLocations.length > 0)}
236                     issue={issueToDisplay}
237                     onClick={this.props.onIssueSelect}
238                     ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
239                   />
240                 )}
241               </IssueSourceViewerScrollContext.Consumer>
242             );
243           })}
244         </div>
245       )
246     );
247   };
248
249   render() {
250     const { branchLike, isLastOccurenceOfPrimaryComponent, issue, lastSnippetGroup, snippetGroup } =
251       this.props;
252     const { additionalLines, loading, snippets } = this.state;
253     const locations =
254       issue.component === snippetGroup.component.key && issue.textRange !== undefined
255         ? locationsByLine([issue])
256         : {};
257
258     const fullyShown =
259       snippets.length === 1 &&
260       snippetGroup.component.measures &&
261       snippets[0].end - snippets[0].start ===
262         parseInt(snippetGroup.component.measures.lines || '', 10);
263
264     const snippetLines = linesForSnippets(snippets, {
265       ...snippetGroup.sources,
266       ...additionalLines,
267     });
268
269     const isFlow = issue.secondaryLocations.length === 0;
270     const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
271
272     const issueIsClosed = issue.status === IssueStatus.Closed;
273     const issueIsFileLevel = issue.componentQualifier === ComponentQualifier.File;
274     const closedIssueMessageKey = issueIsFileLevel
275       ? 'issue.closed.file_level'
276       : 'issue.closed.project_level';
277
278     return (
279       <>
280         {issueIsClosed && (
281           <Alert variant="success">
282             <FormattedMessage
283               id={closedIssueMessageKey}
284               defaultMessage={translate(closedIssueMessageKey)}
285               values={{
286                 status: (
287                   <strong>
288                     {translate('issue.status', issue.status)} (
289                     {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
290                   </strong>
291                 ),
292               }}
293             />
294           </Alert>
295         )}
296
297         <IssueSourceViewerHeader
298           branchLike={branchLike}
299           className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''}
300           expandable={!fullyShown && isFile(snippetGroup.component.q)}
301           loading={loading}
302           onExpand={this.expandComponent}
303           sourceViewerFile={snippetGroup.component}
304         />
305
306         {issue.component === snippetGroup.component.key &&
307           issue.textRange === undefined &&
308           !issueIsClosed && (
309             <IssueSourceViewerScrollContext.Consumer>
310               {(ctx) => (
311                 <IssueMessageBox
312                   selected={true}
313                   issue={issue}
314                   onClick={this.props.onIssueSelect}
315                   ref={ctx?.registerPrimaryLocationRef}
316                 />
317               )}
318             </IssueSourceViewerScrollContext.Consumer>
319           )}
320
321         {snippetLines.map((snippet, index) => (
322           <SnippetViewer
323             key={snippets[index].index}
324             renderAdditionalChildInLine={this.renderIssuesList}
325             component={this.props.snippetGroup.component}
326             duplications={this.props.duplications}
327             duplicationsByLine={this.props.duplicationsByLine}
328             expandBlock={this.expandBlock}
329             handleSymbolClick={this.handleSymbolClick}
330             highlightedLocationMessage={this.props.highlightedLocationMessage}
331             highlightedSymbols={this.state.highlightedSymbols}
332             index={snippets[index].index}
333             issue={this.props.issue}
334             lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1}
335             loadDuplications={this.loadDuplications}
336             locations={this.props.locations}
337             locationsByLine={includeIssueLocation ? locations : {}}
338             onLocationSelect={this.props.onLocationSelect}
339             renderDuplicationPopup={this.renderDuplicationPopup}
340             snippet={snippet}
341           />
342         ))}
343       </>
344     );
345   }
346 }