]> source.dussan.org Git - sonarqube.git/blob
0a0441159804191e001f617eb1e2b03fbc2f85c2
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2019 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 * as classNames from 'classnames';
22 import {
23   createSnippets,
24   expandSnippet,
25   inSnippet,
26   EXPAND_BY_LINES,
27   LINES_BELOW_LAST,
28   MERGE_DISTANCE
29 } from './utils';
30 import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
31 import Line from '../../../components/SourceViewer/components/Line';
32 import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
33 import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
34 import { getSources } from '../../../api/components';
35 import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
36 import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
37 import {
38   optimizeLocationMessage,
39   optimizeHighlightedSymbols,
40   optimizeSelectedIssue
41 } from '../../../components/SourceViewer/helpers/lines';
42 import { translate } from '../../../helpers/l10n';
43
44 interface Props {
45   branchLike: T.BranchLike | undefined;
46   duplications?: T.Duplication[];
47   duplicationsByLine?: { [line: number]: number[] };
48   highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
49   issue: T.Issue;
50   issuePopup?: { issue: string; name: string };
51   issuesByLine: T.IssuesByLine;
52   last: boolean;
53   linePopup?: T.LinePopup;
54   loadDuplications: (component: string, line: T.SourceLine) => void;
55   locations: T.FlowLocation[];
56   onIssueChange: (issue: T.Issue) => void;
57   onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
58   onLinePopupToggle: (linePopup: T.LinePopup & { component: string }) => void;
59   onLocationSelect: (index: number) => void;
60   renderDuplicationPopup: (
61     component: T.SourceViewerFile,
62     index: number,
63     line: number
64   ) => React.ReactNode;
65   scroll?: (element: HTMLElement) => void;
66   snippetGroup: T.SnippetGroup;
67 }
68
69 interface State {
70   additionalLines: { [line: number]: T.SourceLine };
71   highlightedSymbols: string[];
72   loading: boolean;
73   openIssuesByLine: T.Dict<boolean>;
74   snippets: T.SourceLine[][];
75 }
76
77 export default class ComponentSourceSnippetViewer extends React.PureComponent<Props, State> {
78   mounted = false;
79   state: State = {
80     additionalLines: {},
81     highlightedSymbols: [],
82     loading: false,
83     openIssuesByLine: {},
84     snippets: []
85   };
86
87   componentDidMount() {
88     this.mounted = true;
89     this.createSnippetsFromProps();
90   }
91
92   componentWillUnmount() {
93     this.mounted = false;
94   }
95
96   createSnippetsFromProps() {
97     const mainLocation: T.FlowLocation = {
98       component: this.props.issue.component,
99       textRange: this.props.issue.textRange || {
100         endLine: 0,
101         endOffset: 0,
102         startLine: 0,
103         startOffset: 0
104       }
105     };
106     const snippets = createSnippets(
107       this.props.snippetGroup.locations.concat(mainLocation),
108       this.props.snippetGroup.sources,
109       this.props.last
110     );
111     this.setState({ snippets });
112   }
113
114   expandBlock = (snippetIndex: number, direction: T.ExpandDirection) => {
115     const { snippets } = this.state;
116
117     const snippet = snippets[snippetIndex];
118
119     // Extend by EXPAND_BY_LINES and add buffer for merging snippets
120     const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
121
122     const range =
123       direction === 'up'
124         ? {
125             from: Math.max(1, snippet[0].line - extension),
126             to: snippet[0].line - 1
127           }
128         : {
129             from: snippet[snippet.length - 1].line + 1,
130             to: snippet[snippet.length - 1].line + extension
131           };
132
133     getSources({
134       key: this.props.snippetGroup.component.key,
135       ...range
136     })
137       .then(lines =>
138         lines.reduce((lineMap: T.Dict<T.SourceLine>, line) => {
139           line.coverageStatus = getCoverageStatus(line);
140           lineMap[line.line] = line;
141           return lineMap;
142         }, {})
143       )
144       .then(
145         newLinesMapped => {
146           if (this.mounted) {
147             this.setState(({ additionalLines, snippets }) => {
148               const combinedLines = { ...additionalLines, ...newLinesMapped };
149
150               return {
151                 additionalLines: combinedLines,
152                 snippets: expandSnippet({
153                   direction,
154                   lines: { ...combinedLines, ...this.props.snippetGroup.sources },
155                   snippetIndex,
156                   snippets
157                 })
158               };
159             });
160           }
161         },
162         () => {}
163       );
164   };
165
166   expandComponent = () => {
167     const { key } = this.props.snippetGroup.component;
168
169     this.setState({ loading: true });
170
171     getSources({ key }).then(
172       lines => {
173         if (this.mounted) {
174           this.setState({ loading: false, snippets: [lines] });
175         }
176       },
177       () => {
178         if (this.mounted) {
179           this.setState({ loading: false });
180         }
181       }
182     );
183   };
184
185   handleLinePopupToggle = (linePopup: T.LinePopup) => {
186     this.props.onLinePopupToggle({
187       ...linePopup,
188       component: this.props.snippetGroup.component.key
189     });
190   };
191
192   handleOpenIssues = (line: T.SourceLine) => {
193     this.setState(state => ({
194       openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
195     }));
196   };
197
198   handleCloseIssues = (line: T.SourceLine) => {
199     this.setState(state => ({
200       openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
201     }));
202   };
203
204   handleSymbolClick = (highlightedSymbols: string[]) => {
205     this.setState({ highlightedSymbols });
206   };
207
208   loadDuplications = (line: T.SourceLine) => {
209     this.props.loadDuplications(this.props.snippetGroup.component.key, line);
210   };
211
212   renderDuplicationPopup = (index: number, line: number) => {
213     return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
214   };
215
216   renderLine({
217     displayDuplications,
218     index,
219     issuesForLine,
220     issueLocations,
221     line,
222     snippet,
223     symbols,
224     verticalBuffer
225   }: {
226     displayDuplications: boolean;
227     index: number;
228     issuesForLine: T.Issue[];
229     issueLocations: T.LinearIssueLocation[];
230     line: T.SourceLine;
231     snippet: T.SourceLine[];
232     symbols: string[];
233     verticalBuffer: number;
234   }) {
235     const { openIssuesByLine } = this.state;
236     const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
237
238     const { duplications, duplicationsByLine } = this.props;
239     const duplicationsCount = duplications ? duplications.length : 0;
240     const lineDuplications =
241       (duplicationsCount && duplicationsByLine && duplicationsByLine[line.line]) || [];
242
243     const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
244
245     return (
246       <Line
247         branchLike={undefined}
248         displayAllIssues={false}
249         displayCoverage={true}
250         displayDuplications={displayDuplications}
251         displayIssues={!isSinkLine || issuesForLine.length > 1}
252         displayLocationMarkers={true}
253         duplications={lineDuplications}
254         duplicationsCount={duplicationsCount}
255         highlighted={false}
256         highlightedLocationMessage={optimizeLocationMessage(
257           this.props.highlightedLocationMessage,
258           secondaryIssueLocations
259         )}
260         highlightedSymbols={optimizeHighlightedSymbols(symbols, this.state.highlightedSymbols)}
261         issueLocations={issueLocations}
262         issuePopup={this.props.issuePopup}
263         issues={issuesForLine}
264         key={line.line}
265         last={false}
266         line={line}
267         linePopup={this.props.linePopup}
268         loadDuplications={this.loadDuplications}
269         onIssueChange={this.props.onIssueChange}
270         onIssuePopupToggle={this.props.onIssuePopupToggle}
271         onIssueSelect={() => {}}
272         onIssueUnselect={() => {}}
273         onIssuesClose={this.handleCloseIssues}
274         onIssuesOpen={this.handleOpenIssues}
275         onLinePopupToggle={this.handleLinePopupToggle}
276         onLocationSelect={this.props.onLocationSelect}
277         onSymbolClick={this.handleSymbolClick}
278         openIssues={openIssuesByLine[line.line]}
279         previousLine={index > 0 ? snippet[index - 1] : undefined}
280         renderDuplicationPopup={this.renderDuplicationPopup}
281         scroll={this.props.scroll}
282         secondaryIssueLocations={secondaryIssueLocations}
283         selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
284         verticalBuffer={verticalBuffer}
285       />
286     );
287   }
288
289   renderSnippet({
290     snippet,
291     index,
292     issue,
293     issuesByLine = {},
294     locationsByLine,
295     last
296   }: {
297     snippet: T.SourceLine[];
298     index: number;
299     issue: T.Issue;
300     issuesByLine: T.IssuesByLine;
301     locationsByLine: { [line: number]: T.LinearIssueLocation[] };
302     last: boolean;
303   }) {
304     const { component } = this.props.snippetGroup;
305     const lastLine =
306       component.measures && component.measures.lines && parseInt(component.measures.lines, 10);
307
308     const symbols = symbolsByLine(snippet);
309
310     const expandBlock = (direction: T.ExpandDirection) => () => this.expandBlock(index, direction);
311
312     const bottomLine = snippet[snippet.length - 1].line;
313     const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
314     const lowestVisibleIssue = Math.max(
315       ...Object.keys(issuesByLine)
316         .map(k => parseInt(k, 10))
317         .filter(l => inSnippet(l, snippet) && (l === issueLine || this.state.openIssuesByLine[l]))
318     );
319     const verticalBuffer = last
320       ? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
321       : 0;
322
323     const displayDuplications = snippet.some(s => !!s.duplicated);
324
325     return (
326       <div className="source-viewer-code snippet" key={index}>
327         {snippet[0].line > 1 && (
328           <button
329             aria-label={translate('source_viewer.expand_above')}
330             className="expand-block expand-block-above"
331             onClick={expandBlock('up')}
332             type="button">
333             <ExpandSnippetIcon />
334           </button>
335         )}
336         <table className="source-table">
337           <tbody>
338             {snippet.map((line, index) =>
339               this.renderLine({
340                 displayDuplications,
341                 index,
342                 issuesForLine: issuesByLine[line.line] || [],
343                 issueLocations: locationsByLine[line.line] || [],
344                 line,
345                 snippet,
346                 symbols: symbols[line.line],
347                 verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
348               })
349             )}
350           </tbody>
351         </table>
352         {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
353           <button
354             aria-label={translate('source_viewer.expand_below')}
355             className="expand-block expand-block-below"
356             onClick={expandBlock('down')}
357             type="button">
358             <ExpandSnippetIcon />
359           </button>
360         )}
361       </div>
362     );
363   }
364
365   render() {
366     const { branchLike, duplications, issue, issuesByLine, last, snippetGroup } = this.props;
367     const { loading, snippets } = this.state;
368     const locations = locationsByLine([issue]);
369
370     const fullyShown =
371       snippets.length === 1 &&
372       snippetGroup.component.measures &&
373       snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10);
374
375     return (
376       <div
377         className={classNames('component-source-container', {
378           'source-duplications-expanded': duplications && duplications.length > 0
379         })}>
380         <SourceViewerHeaderSlim
381           branchLike={branchLike}
382           expandable={!fullyShown}
383           loading={loading}
384           onExpand={this.expandComponent}
385           sourceViewerFile={snippetGroup.component}
386         />
387         {snippets.map((snippet, index) =>
388           this.renderSnippet({
389             snippet,
390             index,
391             issue,
392             issuesByLine: last ? issuesByLine : {},
393             locationsByLine: last && index === snippets.length - 1 ? locations : {},
394             last: last && index === snippets.length - 1
395           })
396         )}
397       </div>
398     );
399   }
400 }