Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

ComponentSourceSnippetGroupViewer.tsx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
  21. import classNames from 'classnames';
  22. import { FlagMessage, IssueMessageHighlighting, LineFinding, themeColor } from 'design-system';
  23. import * as React from 'react';
  24. import { FormattedMessage } from 'react-intl';
  25. import { getSources } from '../../../api/components';
  26. import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
  27. import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
  28. import { getBranchLikeQuery } from '../../../helpers/branch-like';
  29. import { translate } from '../../../helpers/l10n';
  30. import { BranchLike } from '../../../types/branch-like';
  31. import { isFile } from '../../../types/component';
  32. import { IssueDeprecatedStatus } from '../../../types/issues';
  33. import {
  34. Dict,
  35. Duplication,
  36. ExpandDirection,
  37. FlowLocation,
  38. IssuesByLine,
  39. Snippet,
  40. SnippetGroup,
  41. SourceLine,
  42. SourceViewerFile,
  43. Issue as TypeIssue,
  44. } from '../../../types/types';
  45. import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext';
  46. import { IssueSourceViewerHeader } from './IssueSourceViewerHeader';
  47. import SnippetViewer from './SnippetViewer';
  48. import {
  49. EXPAND_BY_LINES,
  50. MERGE_DISTANCE,
  51. createSnippets,
  52. expandSnippet,
  53. getPrimaryLocation,
  54. linesForSnippets,
  55. } from './utils';
  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. loadDuplications: (component: string, line: SourceLine) => void;
  65. locations: FlowLocation[];
  66. onIssueSelect: (issueKey: string) => void;
  67. onLocationSelect: (index: number) => void;
  68. renderDuplicationPopup: (
  69. component: SourceViewerFile,
  70. index: number,
  71. line: number,
  72. ) => React.ReactNode;
  73. snippetGroup: SnippetGroup;
  74. }
  75. interface State {
  76. additionalLines: { [line: number]: SourceLine };
  77. highlightedSymbols: string[];
  78. loading: boolean;
  79. snippets: Snippet[];
  80. }
  81. export default class ComponentSourceSnippetGroupViewer extends React.PureComponent<
  82. Readonly<Props>,
  83. State
  84. > {
  85. mounted = false;
  86. constructor(props: Readonly<Props>) {
  87. super(props);
  88. this.state = {
  89. additionalLines: {},
  90. highlightedSymbols: [],
  91. loading: false,
  92. snippets: [],
  93. };
  94. }
  95. componentDidMount() {
  96. this.mounted = true;
  97. this.createSnippetsFromProps();
  98. }
  99. componentWillUnmount() {
  100. this.mounted = false;
  101. }
  102. createSnippetsFromProps() {
  103. const { issue, snippetGroup } = this.props;
  104. const locations = [...snippetGroup.locations];
  105. // Add primary location if the component matches
  106. if (issue.component === snippetGroup.component.key) {
  107. locations.unshift(getPrimaryLocation(issue));
  108. }
  109. const snippets = createSnippets({
  110. component: snippetGroup.component.key,
  111. issue,
  112. locations,
  113. });
  114. this.setState({ snippets });
  115. }
  116. expandBlock = (snippetIndex: number, direction: ExpandDirection): Promise<void> => {
  117. const { branchLike, snippetGroup } = this.props;
  118. const { key } = snippetGroup.component;
  119. const { snippets } = this.state;
  120. const snippet = snippets.find((s) => s.index === snippetIndex);
  121. if (!snippet) {
  122. return Promise.reject();
  123. }
  124. // Extend by EXPAND_BY_LINES and add buffer for merging snippets
  125. const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
  126. const range =
  127. direction === 'up'
  128. ? {
  129. from: Math.max(1, snippet.start - extension),
  130. to: snippet.start - 1,
  131. }
  132. : {
  133. from: snippet.end + 1,
  134. to: snippet.end + extension,
  135. };
  136. return getSources({
  137. key,
  138. ...range,
  139. ...getBranchLikeQuery(branchLike),
  140. })
  141. .then((lines) =>
  142. lines.reduce((lineMap: Dict<SourceLine>, line) => {
  143. line.coverageStatus = getCoverageStatus(line);
  144. lineMap[line.line] = line;
  145. return lineMap;
  146. }, {}),
  147. )
  148. .then((newLinesMapped) => {
  149. const newSnippets = expandSnippet({
  150. direction,
  151. snippetIndex,
  152. snippets,
  153. });
  154. this.setState(({ additionalLines }) => {
  155. const combinedLines = { ...additionalLines, ...newLinesMapped };
  156. return {
  157. additionalLines: combinedLines,
  158. snippets: newSnippets.filter((s) => !s.toDelete),
  159. };
  160. });
  161. });
  162. };
  163. expandComponent = () => {
  164. const { branchLike, snippetGroup } = this.props;
  165. const { key } = snippetGroup.component;
  166. this.setState({ loading: true });
  167. getSources({ key, ...getBranchLikeQuery(branchLike) }).then(
  168. (lines) => {
  169. if (this.mounted) {
  170. this.setState(({ additionalLines }) => {
  171. const combinedLines = { ...additionalLines, ...lines };
  172. return {
  173. additionalLines: combinedLines,
  174. loading: false,
  175. snippets: [{ start: 0, end: lines[lines.length - 1].line, index: -1 }],
  176. };
  177. });
  178. }
  179. },
  180. () => {
  181. if (this.mounted) {
  182. this.setState({ loading: false });
  183. }
  184. },
  185. );
  186. };
  187. handleSymbolClick = (clickedSymbols: string[]) => {
  188. this.setState(({ highlightedSymbols }) => {
  189. const newHighlightedSymbols = clickedSymbols.filter(
  190. (symb) => !highlightedSymbols.includes(symb),
  191. );
  192. return { highlightedSymbols: newHighlightedSymbols };
  193. });
  194. };
  195. loadDuplications = (line: SourceLine) => {
  196. this.props.loadDuplications(this.props.snippetGroup.component.key, line);
  197. };
  198. renderDuplicationPopup = (index: number, line: number) => {
  199. return this.props.renderDuplicationPopup(this.props.snippetGroup.component, index, line);
  200. };
  201. renderIssuesList = (line: SourceLine) => {
  202. const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props;
  203. const locations =
  204. issue.component === snippetGroup.component.key && issue.textRange !== undefined
  205. ? locationsByLine([issue])
  206. : {};
  207. const isFlow = issue.secondaryLocations.length === 0;
  208. const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
  209. const issueLocations = includeIssueLocation ? locations[line.line] : [];
  210. const issuesForLine = (issuesByLine[line.line] || []).filter(
  211. (issueForline) =>
  212. issue.key !== issueForline.key ||
  213. (issue.key === issueForline.key && issueLocations.length > 0),
  214. );
  215. return (
  216. issuesForLine.length > 0 && (
  217. <div>
  218. {issuesForLine.map((issueToDisplay) => {
  219. const isSelectedIssue = issueToDisplay.key === issue.key;
  220. return (
  221. <IssueSourceViewerScrollContext.Consumer key={issueToDisplay.key}>
  222. {(ctx) => (
  223. <LineFinding
  224. issueKey={issueToDisplay.key}
  225. message={
  226. <IssueMessageHighlighting
  227. message={issueToDisplay.message}
  228. messageFormattings={issueToDisplay.messageFormattings}
  229. />
  230. }
  231. selected={isSelectedIssue}
  232. ref={isSelectedIssue ? ctx?.registerPrimaryLocationRef : undefined}
  233. onIssueSelect={this.props.onIssueSelect}
  234. />
  235. )}
  236. </IssueSourceViewerScrollContext.Consumer>
  237. );
  238. })}
  239. </div>
  240. )
  241. );
  242. };
  243. render() {
  244. const { isLastOccurenceOfPrimaryComponent, issue, snippetGroup } = this.props;
  245. const { additionalLines, loading, snippets } = this.state;
  246. const snippetLines = linesForSnippets(snippets, {
  247. ...snippetGroup.sources,
  248. ...additionalLines,
  249. });
  250. const issueIsClosed = issue.status === IssueDeprecatedStatus.Closed;
  251. const issueIsFileLevel = isFile(issue.componentQualifier) && issue.componentEnabled;
  252. const closedIssueMessageKey = issueIsFileLevel
  253. ? 'issue.closed.file_level'
  254. : 'issue.closed.project_level';
  255. const hideLocationIndex = issue.secondaryLocations.length !== 0;
  256. return (
  257. <>
  258. {issueIsClosed && (
  259. <FlagMessage className="sw-mb-2 sw-flex" variant="success">
  260. <div className="sw-block">
  261. <FormattedMessage
  262. id={closedIssueMessageKey}
  263. defaultMessage={translate(closedIssueMessageKey)}
  264. values={{
  265. status: (
  266. <strong>
  267. {translate('issue.status', issue.status)} (
  268. {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
  269. </strong>
  270. ),
  271. }}
  272. />
  273. </div>
  274. </FlagMessage>
  275. )}
  276. <IssueSourceViewerHeader
  277. className={issueIsClosed && !issueIsFileLevel ? 'null-spacer-bottom' : ''}
  278. expandable={isExpandable(snippets, snippetGroup)}
  279. issueKey={issue.key}
  280. loading={loading}
  281. onExpand={this.expandComponent}
  282. sourceViewerFile={snippetGroup.component}
  283. />
  284. {issue.component === snippetGroup.component.key &&
  285. issue.textRange === undefined &&
  286. !issueIsClosed && (
  287. <FileLevelIssueStyle className="sw-py-2">
  288. <IssueSourceViewerScrollContext.Consumer>
  289. {(ctx) => (
  290. <LineFinding
  291. issueKey={issue.key}
  292. message={
  293. <IssueMessageHighlighting
  294. message={issue.message}
  295. messageFormattings={issue.messageFormattings}
  296. />
  297. }
  298. selected
  299. ref={ctx?.registerPrimaryLocationRef}
  300. onIssueSelect={this.props.onIssueSelect}
  301. className="sw-m-0 sw-cursor-default"
  302. />
  303. )}
  304. </IssueSourceViewerScrollContext.Consumer>
  305. </FileLevelIssueStyle>
  306. )}
  307. {snippetLines.map(({ snippet, sourcesMap }, index) => (
  308. <SnippetViewer
  309. key={snippets[index].index}
  310. renderAdditionalChildInLine={this.renderIssuesList}
  311. component={this.props.snippetGroup.component}
  312. duplications={this.props.duplications}
  313. duplicationsByLine={this.props.duplicationsByLine}
  314. expandBlock={this.expandBlock}
  315. handleSymbolClick={this.handleSymbolClick}
  316. highlightedLocationMessage={this.props.highlightedLocationMessage}
  317. highlightedSymbols={this.state.highlightedSymbols}
  318. index={snippets[index].index}
  319. loadDuplications={this.loadDuplications}
  320. locations={this.props.locations}
  321. locationsByLine={getLocationsByLine(
  322. issue,
  323. snippetGroup,
  324. isLastOccurenceOfPrimaryComponent,
  325. )}
  326. onLocationSelect={this.props.onLocationSelect}
  327. renderDuplicationPopup={this.renderDuplicationPopup}
  328. snippet={snippet}
  329. className={classNames({ 'sw-mt-2': index !== 0 })}
  330. snippetSourcesMap={sourcesMap}
  331. hideLocationIndex={hideLocationIndex}
  332. />
  333. ))}
  334. </>
  335. );
  336. }
  337. }
  338. function getLocationsByLine(
  339. issue: TypeIssue,
  340. snippetGroup: SnippetGroup,
  341. isLastOccurenceOfPrimaryComponent: boolean,
  342. ) {
  343. const isFlow = issue.secondaryLocations.length === 0;
  344. const includeIssueLocation = isFlow ? isLastOccurenceOfPrimaryComponent : true;
  345. return includeIssueLocation &&
  346. issue.component === snippetGroup.component.key &&
  347. issue.textRange !== undefined
  348. ? locationsByLine([issue])
  349. : {};
  350. }
  351. function isExpandable(snippets: Snippet[], snippetGroup: SnippetGroup) {
  352. const fullyShown =
  353. snippets.length === 1 &&
  354. snippetGroup.component.measures &&
  355. snippets[0].end - snippets[0].start ===
  356. parseInt(snippetGroup.component.measures.lines ?? '', 10);
  357. return !fullyShown && isFile(snippetGroup.component.q);
  358. }
  359. const FileLevelIssueStyle = styled.div`
  360. border: 1px solid ${themeColor('codeLineBorder')};
  361. `;