]> source.dussan.org Git - sonarqube.git/blob
2630d4b1adddf76221b06ea5d87dcf09708ae576
[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 issueLocations = includeIssueLocation ? locations[line.line] : [];
224     const issuesForLine = (issuesByLine[line.line] || []).filter(
225       (issueForline) =>
226         issue.key !== issueForline.key ||
227         (issue.key === issueForline.key && issueLocations.length > 0)
228     );
229
230     return (
231       issuesForLine.length > 0 && (
232         <div>
233           {issuesForLine.map((issueToDisplay) => {
234             const isSelectedIssue = issueToDisplay.key === issue.key;
235             return (
236               <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
237                 {(ctx) => (
238                   <IssueMessageBox
239                     selected={isSelectedIssue}
240                     issue={issueToDisplay}
241                     onClick={this.props.onIssueSelect}
242                     ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
243                   />
244                 )}
245               </IssueSourceViewerScrollContext.Consumer>
246             );
247           })}
248         </div>
249       )
250     );
251   };
252
253   render() {
254     const { branchLike, isLastOccurenceOfPrimaryComponent, issue, lastSnippetGroup, snippetGroup } =
255       this.props;
256     const { additionalLines, loading, snippets } = this.state;
257     const locations =
258       issue.component === snippetGroup.component.key && issue.textRange !== undefined
259         ? locationsByLine([issue])
260         : {};
261
262     const fullyShown =
263       snippets.length === 1 &&
264       snippetGroup.component.measures &&
265       snippets[0].end - snippets[0].start ===
266         parseInt(snippetGroup.component.measures.lines || '', 10);
267
268     const snippetLines = linesForSnippets(snippets, {
269       ...snippetGroup.sources,
270       ...additionalLines,
271     });
272
273     const isFlow = issue.secondaryLocations.length === 0;
274     const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
275
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';
281
282     return (
283       <>
284         {issueIsClosed && (
285           <Alert variant="success">
286             <FormattedMessage
287               id={closedIssueMessageKey}
288               defaultMessage={translate(closedIssueMessageKey)}
289               values={{
290                 status: (
291                   <strong>
292                     {translate('issue.status', issue.status)} (
293                     {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
294                   </strong>
295                 ),
296               }}
297             />
298           </Alert>
299         )}
300
301         <IssueSourceViewerHeader
302           branchLike={branchLike}
303           className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''}
304           expandable={!fullyShown && isFile(snippetGroup.component.q)}
305           loading={loading}
306           onExpand={this.expandComponent}
307           sourceViewerFile={snippetGroup.component}
308         />
309
310         {issue.component === snippetGroup.component.key &&
311           issue.textRange === undefined &&
312           !issueIsClosed && (
313             <IssueSourceViewerScrollContext.Consumer>
314               {(ctx) => (
315                 <IssueMessageBox
316                   selected={true}
317                   issue={issue}
318                   onClick={this.props.onIssueSelect}
319                   ref={ctx?.registerPrimaryLocationRef}
320                 />
321               )}
322             </IssueSourceViewerScrollContext.Consumer>
323           )}
324
325         {snippetLines.map((snippet, index) => (
326           <SnippetViewer
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}
344             snippet={snippet}
345           />
346         ))}
347       </>
348     );
349   }
350 }