]> source.dussan.org Git - sonarqube.git/blob
676c7701ec6a63f001bc99ab0a5eb1b2f190372c
[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 { findLastIndex, keyBy } from 'lodash';
21 import * as React from 'react';
22 import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';
23 import { getIssueFlowSnippets } from '../../../api/issues';
24 import DuplicationPopup from '../../../components/SourceViewer/components/DuplicationPopup';
25 import {
26   filterDuplicationBlocksByLine,
27   getDuplicationBlocksForIndex,
28   isDuplicationBlockInRemovedComponent
29 } from '../../../components/SourceViewer/helpers/duplications';
30 import {
31   duplicationsByLine as getDuplicationsByLine,
32   issuesByComponentAndLine
33 } from '../../../components/SourceViewer/helpers/indexing';
34 import { SourceViewerContext } from '../../../components/SourceViewer/SourceViewerContext';
35 import { Alert } from '../../../components/ui/Alert';
36 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
37 import { WorkspaceContext } from '../../../components/workspace/context';
38 import { getBranchLikeQuery } from '../../../helpers/branch-like';
39 import { throwGlobalError } from '../../../helpers/error';
40 import { translate } from '../../../helpers/l10n';
41 import { BranchLike } from '../../../types/branch-like';
42 import { isFile } from '../../../types/component';
43 import {
44   Dict,
45   DuplicatedFile,
46   Duplication,
47   FlowLocation,
48   Issue,
49   SnippetsByComponent,
50   SourceViewerFile
51 } from '../../../types/types';
52 import ComponentSourceSnippetGroupViewer from './ComponentSourceSnippetGroupViewer';
53 import { getPrimaryLocation, groupLocationsByComponent } from './utils';
54
55 interface Props {
56   branchLike: BranchLike | undefined;
57   highlightedLocationMessage?: { index: number; text: string | undefined };
58   issue: Issue;
59   issues: Issue[];
60   locations: FlowLocation[];
61   onIssueChange: (issue: Issue) => void;
62   onLoaded?: () => void;
63   onLocationSelect: (index: number) => void;
64   scroll?: (element: HTMLElement) => void;
65   selectedFlowIndex: number | undefined;
66 }
67
68 interface State {
69   components: Dict<SnippetsByComponent>;
70   duplicatedFiles?: Dict<DuplicatedFile>;
71   duplications?: Duplication[];
72   duplicationsByLine: { [line: number]: number[] };
73   issuePopup?: { issue: string; name: string };
74   loading: boolean;
75   notAccessible: boolean;
76 }
77
78 export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
79   mounted = false;
80   state: State = {
81     components: {},
82     duplicationsByLine: {},
83     loading: true,
84     notAccessible: false
85   };
86
87   componentDidMount() {
88     this.mounted = true;
89     this.fetchIssueFlowSnippets();
90   }
91
92   componentDidUpdate(prevProps: Props) {
93     if (prevProps.issue.key !== this.props.issue.key) {
94       this.fetchIssueFlowSnippets();
95     }
96   }
97
98   componentWillUnmount() {
99     this.mounted = false;
100   }
101
102   fetchDuplications = (component: string) => {
103     getDuplications({
104       key: component,
105       ...getBranchLikeQuery(this.props.branchLike)
106     }).then(
107       r => {
108         if (this.mounted) {
109           this.setState({
110             duplicatedFiles: r.files,
111             duplications: r.duplications,
112             duplicationsByLine: getDuplicationsByLine(r.duplications)
113           });
114         }
115       },
116       () => {}
117     );
118   };
119
120   async fetchIssueFlowSnippets() {
121     const { issue, branchLike } = this.props;
122     this.setState({ loading: true });
123
124     try {
125       const components = await getIssueFlowSnippets(issue.key);
126       if (components[issue.component] === undefined) {
127         const issueComponent = await getComponentForSourceViewer({
128           component: issue.component,
129           ...getBranchLikeQuery(branchLike)
130         });
131         components[issue.component] = { component: issueComponent, sources: [] };
132         if (isFile(issueComponent.q)) {
133           const sources = await getSources({
134             key: issueComponent.key,
135             ...getBranchLikeQuery(branchLike),
136             from: 1,
137             to: 10
138           }).then(lines => keyBy(lines, 'line'));
139           components[issue.component].sources = sources;
140         }
141       }
142       if (this.mounted) {
143         this.setState({
144           components,
145           issuePopup: undefined,
146           loading: false
147         });
148         if (this.props.onLoaded) {
149           this.props.onLoaded();
150         }
151       }
152     } catch (response) {
153       const rsp = response as Response;
154       if (rsp.status !== 403) {
155         throwGlobalError(response);
156       }
157       if (this.mounted) {
158         this.setState({ loading: false, notAccessible: rsp.status === 403 });
159       }
160     }
161   }
162
163   handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
164     this.setState((state: State) => {
165       const samePopup =
166         state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
167       if (open !== false && !samePopup) {
168         return { issuePopup: { issue, name: popupName } };
169       } else if (open !== true && samePopup) {
170         return { issuePopup: undefined };
171       }
172       return null;
173     });
174   };
175
176   renderDuplicationPopup = (component: SourceViewerFile, index: number, line: number) => {
177     const { duplicatedFiles, duplications } = this.state;
178
179     if (!component || !duplicatedFiles) {
180       return null;
181     }
182
183     const blocks = getDuplicationBlocksForIndex(duplications, index);
184
185     return (
186       <WorkspaceContext.Consumer>
187         {({ openComponent }) => (
188           <DuplicationPopup
189             blocks={filterDuplicationBlocksByLine(blocks, line)}
190             branchLike={this.props.branchLike}
191             duplicatedFiles={duplicatedFiles}
192             inRemovedComponent={isDuplicationBlockInRemovedComponent(blocks)}
193             openComponent={openComponent}
194             sourceViewerFile={component}
195           />
196         )}
197       </WorkspaceContext.Consumer>
198     );
199   };
200
201   render() {
202     const { loading, notAccessible } = this.state;
203
204     if (loading) {
205       return (
206         <div>
207           <DeferredSpinner />
208         </div>
209       );
210     }
211
212     if (notAccessible) {
213       return (
214         <Alert className="spacer-top" variant="warning">
215           {translate('code_viewer.no_source_code_displayed_due_to_security')}
216         </Alert>
217       );
218     }
219
220     const { issue, locations } = this.props;
221     const { components, duplications, duplicationsByLine } = this.state;
222     const issuesByComponent = issuesByComponentAndLine(this.props.issues);
223     const locationsByComponent = groupLocationsByComponent(issue, locations, components);
224
225     const lastOccurenceOfPrimaryComponent = findLastIndex(
226       locationsByComponent,
227       ({ component }) => component.key === issue.component
228     );
229
230     if (components[issue.component] === undefined) {
231       return null;
232     }
233
234     return (
235       <div>
236         {locationsByComponent.map((snippetGroup, i) => {
237           return (
238             <SourceViewerContext.Provider
239               // eslint-disable-next-line react/no-array-index-key
240               key={`${issue.key}-${this.props.selectedFlowIndex || 0}-${i}`}
241               value={{ branchLike: this.props.branchLike, file: snippetGroup.component }}>
242               <ComponentSourceSnippetGroupViewer
243                 branchLike={this.props.branchLike}
244                 duplications={duplications}
245                 duplicationsByLine={duplicationsByLine}
246                 highlightedLocationMessage={this.props.highlightedLocationMessage}
247                 issue={issue}
248                 issuePopup={this.state.issuePopup}
249                 issuesByLine={issuesByComponent[snippetGroup.component.key] || {}}
250                 isLastOccurenceOfPrimaryComponent={i === lastOccurenceOfPrimaryComponent}
251                 lastSnippetGroup={i === locationsByComponent.length - 1}
252                 loadDuplications={this.fetchDuplications}
253                 locations={snippetGroup.locations || []}
254                 onIssueChange={this.props.onIssueChange}
255                 onIssuePopupToggle={this.handleIssuePopupToggle}
256                 onLocationSelect={this.props.onLocationSelect}
257                 renderDuplicationPopup={this.renderDuplicationPopup}
258                 scroll={this.props.scroll}
259                 snippetGroup={snippetGroup}
260               />
261             </SourceViewerContext.Provider>
262           );
263         })}
264
265         {locationsByComponent.length === 0 && (
266           <ComponentSourceSnippetGroupViewer
267             branchLike={this.props.branchLike}
268             duplications={duplications}
269             duplicationsByLine={duplicationsByLine}
270             highlightedLocationMessage={this.props.highlightedLocationMessage}
271             issue={issue}
272             issuePopup={this.state.issuePopup}
273             issuesByLine={issuesByComponent[issue.component] || {}}
274             isLastOccurenceOfPrimaryComponent={true}
275             lastSnippetGroup={true}
276             loadDuplications={this.fetchDuplications}
277             locations={[]}
278             onIssueChange={this.props.onIssueChange}
279             onIssuePopupToggle={this.handleIssuePopupToggle}
280             onLocationSelect={this.props.onLocationSelect}
281             renderDuplicationPopup={this.renderDuplicationPopup}
282             scroll={this.props.scroll}
283             snippetGroup={{
284               locations: [getPrimaryLocation(issue)],
285               ...components[issue.component]
286             }}
287           />
288         )}
289       </div>
290     );
291   }
292 }