You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComponentSourceSnippetGroupViewer.tsx 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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 { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext';
  41. import IssueSourceViewerHeader from './IssueSourceViewerHeader';
  42. import SnippetViewer from './SnippetViewer';
  43. import {
  44. createSnippets,
  45. expandSnippet,
  46. EXPAND_BY_LINES,
  47. getPrimaryLocation,
  48. linesForSnippets,
  49. MERGE_DISTANCE
  50. } from './utils';
  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. interface State {
  72. additionalLines: { [line: number]: SourceLine };
  73. highlightedSymbols: string[];
  74. loading: boolean;
  75. snippets: Snippet[];
  76. }
  77. export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<Props, State> {
  78. mounted = false;
  79. constructor(props: Props) {
  80. super(props);
  81. this.state = {
  82. additionalLines: {},
  83. highlightedSymbols: [],
  84. loading: false,
  85. snippets: []
  86. };
  87. }
  88. componentDidMount() {
  89. this.mounted = true;
  90. this.createSnippetsFromProps();
  91. }
  92. componentWillUnmount() {
  93. this.mounted = false;
  94. }
  95. createSnippetsFromProps() {
  96. const { issue, snippetGroup } = this.props;
  97. const snippets = createSnippets({
  98. component: snippetGroup.component.key,
  99. issue,
  100. locations:
  101. snippetGroup.locations.length === 0
  102. ? [getPrimaryLocation(issue)]
  103. : [getPrimaryLocation(issue), ...snippetGroup.locations]
  104. });
  105. this.setState({ snippets });
  106. }
  107. expandBlock = (snippetIndex: number, direction: ExpandDirection): Promise<void> => {
  108. const { branchLike, snippetGroup } = this.props;
  109. const { key } = snippetGroup.component;
  110. const { snippets } = this.state;
  111. const snippet = snippets.find(s => s.index === snippetIndex);
  112. if (!snippet) {
  113. return Promise.reject();
  114. }
  115. // Extend by EXPAND_BY_LINES and add buffer for merging snippets
  116. const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
  117. const range =
  118. direction === 'up'
  119. ? {
  120. from: Math.max(1, snippet.start - extension),
  121. to: snippet.start - 1
  122. }
  123. : {
  124. from: snippet.end + 1,
  125. to: snippet.end + extension
  126. };
  127. return getSources({
  128. key,
  129. ...range,
  130. ...getBranchLikeQuery(branchLike)
  131. })
  132. .then(lines =>
  133. lines.reduce((lineMap: Dict<SourceLine>, line) => {
  134. line.coverageStatus = getCoverageStatus(line);
  135. lineMap[line.line] = line;
  136. return lineMap;
  137. }, {})
  138. )
  139. .then(newLinesMapped => {
  140. const newSnippets = expandSnippet({
  141. direction,
  142. snippetIndex,
  143. snippets
  144. });
  145. this.setState(({ additionalLines }) => {
  146. const combinedLines = { ...additionalLines, ...newLinesMapped };
  147. return {
  148. additionalLines: combinedLines,
  149. snippets: newSnippets.filter(s => !s.toDelete)
  150. };
  151. });
  152. });
  153. };
  154. expandComponent = () => {
  155. const { branchLike, snippetGroup } = this.props;
  156. const { key } = snippetGroup.component;
  157. this.setState({ loading: true });
  158. getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
  159. lines => {
  160. if (this.mounted) {
  161. this.setState(({ additionalLines }) => {
  162. const combinedLines = { ...additionalLines, ...lines };
  163. return {
  164. additionalLines: combinedLines,
  165. loading: false,
  166. snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }]
  167. };
  168. });
  169. }
  170. },
  171. () => {
  172. if (this.mounted) {
  173. this.setState({ loading: false });
  174. }
  175. }
  176. );
  177. };
  178. handleSymbolClick = (clickedSymbols: string[]) => {
  179. this.setState(({ highlightedSymbols }) => {
  180. const newHighlightedSymbols = clickedSymbols.filter(
  181. symb => !highlightedSymbols.includes(symb)
  182. );
  183. return { highlightedSymbols: newHighlightedSymbols };
  184. });
  185. };
  186. loadDuplications = (line: SourceLine) => {
  187. this.props.loadDuplications(this.props.snippetGroup.component.key, line);
  188. };
  189. renderDuplicationPopup = (index: number, line: number) => {
  190. return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
  191. };
  192. renderIssuesList = (line: SourceLine) => {
  193. const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
  194. const locations =
  195. issue.component === snippetGroup.component.key && issue.textRange !== undefined
  196. ? locationsByLine([issue])
  197. : {};
  198. const isFlow = issue.secondaryLocations.length === 0;
  199. const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
  200. const issuesForLine = issuesByLine[line.line] || [];
  201. const issueLocations = includeIssueLocation ? locations[line.line] : [];
  202. return (
  203. issuesForLine.length > 0 && (
  204. <div>
  205. {issuesForLine.map(issueToDisplay => {
  206. const isSelectedIssue = issueToDisplay.key === issue.key;
  207. return (
  208. <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
  209. {ctx => (
  210. <IssueMessageBox
  211. selected={!!(isSelectedIssue && issueLocations.length > 0)}
  212. issue={issueToDisplay}
  213. onClick={this.props.onIssueSelect}
  214. ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
  215. />
  216. )}
  217. </IssueSourceViewerScrollContext.Consumer>
  218. );
  219. })}
  220. </div>
  221. )
  222. );
  223. };
  224. render() {
  225. const {
  226. branchLike,
  227. isLastOccurenceOfPrimaryComponent,
  228. issue,
  229. lastSnippetGroup,
  230. snippetGroup
  231. } = this.props;
  232. const { additionalLines, loading, snippets } = this.state;
  233. const locations =
  234. issue.component === snippetGroup.component.key && issue.textRange !== undefined
  235. ? locationsByLine([issue])
  236. : {};
  237. const fullyShown =
  238. snippets.length === 1 &&
  239. snippetGroup.component.measures &&
  240. snippets[0].end - snippets[0].start ===
  241. parseInt(snippetGroup.component.measures.lines || '', 10);
  242. const snippetLines = linesForSnippets(snippets, {
  243. ...snippetGroup.sources,
  244. ...additionalLines
  245. });
  246. const isFlow = issue.secondaryLocations.length === 0;
  247. const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
  248. return (
  249. <>
  250. <IssueSourceViewerHeader
  251. branchLike={branchLike}
  252. expandable={!fullyShown && isFile(snippetGroup.component.q)}
  253. loading={loading}
  254. onExpand={this.expandComponent}
  255. sourceViewerFile={snippetGroup.component}
  256. />
  257. {issue.component === snippetGroup.component.key && issue.textRange === undefined && (
  258. <IssueSourceViewerScrollContext.Consumer>
  259. {ctx => (
  260. <IssueMessageBox
  261. selected={true}
  262. issue={issue}
  263. onClick={this.props.onIssueSelect}
  264. ref={ctx?.registerPrimaryLocationRef}
  265. />
  266. )}
  267. </IssueSourceViewerScrollContext.Consumer>
  268. )}
  269. {snippetLines.map((snippet, index) => (
  270. <SnippetViewer
  271. key={snippets[index].index}
  272. renderAdditionalChildInLine={this.renderIssuesList}
  273. component={this.props.snippetGroup.component}
  274. duplications={this.props.duplications}
  275. duplicationsByLine={this.props.duplicationsByLine}
  276. expandBlock={this.expandBlock}
  277. handleSymbolClick={this.handleSymbolClick}
  278. highlightedLocationMessage={this.props.highlightedLocationMessage}
  279. highlightedSymbols={this.state.highlightedSymbols}
  280. index={snippets[index].index}
  281. issue={this.props.issue}
  282. lastSnippetOfLastGroup={lastSnippetGroup && index === snippets.length - 1}
  283. loadDuplications={this.loadDuplications}
  284. locations={this.props.locations}
  285. locationsByLine={includeIssueLocation ? locations : {}}
  286. onLocationSelect={this.props.onLocationSelect}
  287. renderDuplicationPopup={this.renderDuplicationPopup}
  288. snippet={snippet}
  289. />
  290. ))}
  291. </>
  292. );
  293. }
  294. }