]> source.dussan.org Git - sonarqube.git/blob
6f1068173166bfa5225d341d1c84b062a3948686
[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 { 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';
28 import {
29   Dict,
30   Duplication,
31   ExpandDirection,
32   FlowLocation,
33   Issue as TypeIssue,
34   IssuesByLine,
35   Snippet,
36   SnippetGroup,
37   SourceLine,
38   SourceViewerFile
39 } from '../../../types/types';
40 import IssueSourceViewerHeader from './IssueSourceViewerHeader';
41 import SnippetViewer from './SnippetViewer';
42 import {
43   createSnippets,
44   expandSnippet,
45   EXPAND_BY_LINES,
46   getPrimaryLocation,
47   linesForSnippets,
48   MERGE_DISTANCE
49 } from './utils';
50
51 interface Props {
52   branchLike: BranchLike | undefined;
53   duplications?: Duplication[];
54   duplicationsByLine?: { [line: number]: number[] };
55   highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
56   isLastOccurenceOfPrimaryComponent: boolean;
57   issue: TypeIssue;
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,
66     index: number,
67     line: number
68   ) => React.ReactNode;
69   snippetGroup: SnippetGroup;
70 }
71
72 interface State {
73   additionalLines: { [line: number]: SourceLine };
74   highlightedSymbols: string[];
75   loading: boolean;
76   snippets: Snippet[];
77 }
78
79 export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> {
80   mounted = false;
81
82   constructor(props: Props) {
83     super(props);
84     this.state = {
85       additionalLines: {},
86       highlightedSymbols: [],
87       loading: false,
88       snippets: []
89     };
90   }
91
92   componentDidMount() {
93     this.mounted = true;
94     this.createSnippetsFromProps();
95   }
96
97   componentWillUnmount() {
98     this.mounted = false;
99   }
100
101   createSnippetsFromProps() {
102     const { issue, snippetGroup } = this.props;
103
104     const snippets = createSnippets({
105       component: snippetGroup.component.key,
106       issue,
107       locations:
108         snippetGroup.locations.length === 0 ? [getPrimaryLocation(issue)] : snippetGroup.locations
109     });
110
111     this.setState({ snippets });
112   }
113
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);
119     if (!snippet) {
120       return Promise.reject();
121     }
122     // Extend by EXPAND_BY_LINES and add buffer for merging snippets
123     const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
124     const range =
125       direction === 'up'
126         ? {
127             from: Math.max(1, snippet.start - extension),
128             to: snippet.start - 1
129           }
130         : {
131             from: snippet.end + 1,
132             to: snippet.end + extension
133           };
134     return getSources({
135       key,
136       ...range,
137       ...getBranchLikeQuery(branchLike)
138     })
139       .then(lines =>
140         lines.reduce((lineMap: Dict<SourceLine>, line) => {
141           line.coverageStatus = getCoverageStatus(line);
142           lineMap[line.line] = line;
143           return lineMap;
144         }, {})
145       )
146       .then(newLinesMapped => {
147         const newSnippets = expandSnippet({
148           direction,
149           snippetIndex,
150           snippets
151         });
152
153         this.setState(({ additionalLines }) => {
154           const combinedLines = { ...additionalLines, ...newLinesMapped };
155           return {
156             additionalLines: combinedLines,
157             snippets: newSnippets.filter(s => !s.toDelete)
158           };
159         });
160       });
161   };
162
163   expandComponent = () => {
164     const { branchLike, snippetGroup } = this.props;
165     const { key } = snippetGroup.component;
166
167     this.setState({ loading: true });
168
169     getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
170       lines => {
171         if (this.mounted) {
172           this.setState(({ additionalLines }) => {
173             const combinedLines = { ...additionalLines, ...lines };
174             return {
175               additionalLines: combinedLines,
176               loading: false,
177               snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }]
178             };
179           });
180         }
181       },
182       () => {
183         if (this.mounted) {
184           this.setState({ loading: false });
185         }
186       }
187     );
188   };
189
190   handleSymbolClick = (clickedSymbols: string[]) => {
191     this.setState(({ highlightedSymbols }) => {
192       const newHighlightedSymbols = clickedSymbols.filter(
193         symb => !highlightedSymbols.includes(symb)
194       );
195       return { highlightedSymbols: newHighlightedSymbols };
196     });
197   };
198
199   loadDuplications = (line: SourceLine) => {
200     this.props.loadDuplications(this.props.snippetGroup.component.key, line);
201   };
202
203   renderDuplicationPopup = (index: number, line: number) => {
204     return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
205   };
206
207   renderIssuesList = (line: SourceLine) => {
208     const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
209     const locations =
210       issue.component === snippetGroup.component.key && issue.textRange !== undefined
211         ? locationsByLine([issue])
212         : {};
213
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] : [];
218
219     return (
220       issuesForLine.length > 0 && (
221         <div>
222           {issuesForLine.map(issueToDisplay => (
223             <IssueMessageBox
224               selected={!!(issueToDisplay.key === issue.key && issueLocations.length > 0)}
225               key={issueToDisplay.key}
226               issue={issueToDisplay}
227               onClick={this.props.onIssueSelect}
228             />
229           ))}
230         </div>
231       )
232     );
233   };
234
235   render() {
236     const {
237       branchLike,
238       isLastOccurenceOfPrimaryComponent,
239       issue,
240       lastSnippetGroup,
241       snippetGroup
242     } = this.props;
243     const { additionalLines, loading, snippets } = this.state;
244     const locations =
245       issue.component === snippetGroup.component.key && issue.textRange !== undefined
246         ? locationsByLine([issue])
247         : {};
248
249     const fullyShown =
250       snippets.length === 1 &&
251       snippetGroup.component.measures &&
252       snippets[0].end - snippets[0].start ===
253         parseInt(snippetGroup.component.measures.lines || '', 10);
254
255     const snippetLines = linesForSnippets(snippets, {
256       ...snippetGroup.sources,
257       ...additionalLines
258     });
259
260     const isFlow = issue.secondaryLocations.length === 0;
261     const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
262
263     return (
264       <>
265         <IssueSourceViewerHeader
266           branchLike={branchLike}
267           expandable={!fullyShown && isFile(snippetGroup.component.q)}
268           loading={loading}
269           onExpand={this.expandComponent}
270           sourceViewerFile={snippetGroup.component}
271         />
272
273         {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
274           <IssueMessageBox selected={true} issue={issue} onClick={this.props.onIssueSelect} />
275         )}
276         {snippetLines.map((snippet, index) => (
277           <SnippetViewer
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}
287             index={index}
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}
295             snippet={snippet}
296           />
297         ))}
298       </>
299     );
300   }
301 }