2020-06-30 09:22:25 +02:00
|
|
|
import React from 'react';
|
2020-07-09 18:53:51 +02:00
|
|
|
|
|
|
|
import { makeStyles } from '@material-ui/core/styles';
|
2020-06-29 23:43:49 +02:00
|
|
|
import Container from '@material-ui/core/Container';
|
|
|
|
import Typography from '@material-ui/core/Typography';
|
2020-06-30 14:19:29 +02:00
|
|
|
import Tabs from '@material-ui/core/Tabs';
|
|
|
|
import Tab from '@material-ui/core/Tab';
|
2020-07-09 18:53:51 +02:00
|
|
|
import Box from '@material-ui/core/Box';
|
2020-09-25 13:11:49 +02:00
|
|
|
import { Link } from '@material-ui/core';
|
2021-09-30 12:20:26 +02:00
|
|
|
import { Link as RouterLink } from 'react-router-dom';
|
2020-07-09 18:53:51 +02:00
|
|
|
|
2020-06-30 14:19:29 +02:00
|
|
|
import { useHistory } from "react-router-dom";
|
2020-06-29 23:43:49 +02:00
|
|
|
|
2020-07-09 18:53:51 +02:00
|
|
|
import { useRuleCoverage } from './utils/useRuleCoverage';
|
|
|
|
import { useFetch } from './utils/useFetch';
|
2021-01-26 22:10:28 +01:00
|
|
|
import { RuleMetadata } from './types';
|
2020-07-09 18:53:51 +02:00
|
|
|
|
2020-06-29 23:43:49 +02:00
|
|
|
|
|
|
|
const useStyles = makeStyles((theme) => ({
|
2020-07-09 18:53:51 +02:00
|
|
|
ruleBar: {
|
|
|
|
borderBottom: '1px solid lightgrey',
|
|
|
|
},
|
|
|
|
ruleid: {
|
|
|
|
textAlign: 'center',
|
|
|
|
marginTop: theme.spacing(3),
|
|
|
|
marginBottom: theme.spacing(3),
|
|
|
|
},
|
2021-09-30 12:20:26 +02:00
|
|
|
ruleidLink: {
|
|
|
|
color: 'inherit',
|
|
|
|
},
|
2020-07-09 18:53:51 +02:00
|
|
|
title: {
|
|
|
|
textAlign: 'justify',
|
|
|
|
marginTop: theme.spacing(4),
|
|
|
|
marginBottom: theme.spacing(4),
|
|
|
|
},
|
|
|
|
coverage: {
|
|
|
|
marginBottom: theme.spacing(3),
|
|
|
|
},
|
2020-06-29 23:43:49 +02:00
|
|
|
description: {
|
2020-07-09 18:53:51 +02:00
|
|
|
textAlign: 'justify',
|
|
|
|
// marginBottom: theme.spacing(3),
|
2020-06-30 14:19:29 +02:00
|
|
|
},
|
2020-07-09 18:53:51 +02:00
|
|
|
|
|
|
|
// style used to center the tabs when there too few of them to fill the container
|
|
|
|
tabRoot: {
|
|
|
|
justifyContent: "center"
|
2020-06-30 14:19:29 +02:00
|
|
|
},
|
2020-07-09 18:53:51 +02:00
|
|
|
tabScroller: {
|
2021-01-26 22:10:28 +01:00
|
|
|
flexGrow: 0
|
2021-05-20 11:24:59 +02:00
|
|
|
},
|
|
|
|
unimplemented: {
|
|
|
|
color: 'red'
|
2021-09-30 12:20:26 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
tab: {
|
|
|
|
display: 'flex',
|
|
|
|
|
|
|
|
"&::before": {
|
|
|
|
content: '""',
|
|
|
|
display: 'block',
|
|
|
|
width: theme.spacing(1),
|
|
|
|
height: theme.spacing(1),
|
|
|
|
marginRight: theme.spacing(1),
|
|
|
|
borderRadius: theme.spacing(1),
|
|
|
|
},
|
|
|
|
|
|
|
|
'& > .MuiTab-wrapper': {
|
|
|
|
width: 'auto',
|
|
|
|
}
|
|
|
|
},
|
|
|
|
tabCovered: {
|
|
|
|
"&::before": {
|
|
|
|
backgroundColor: '#4c9bd6',
|
|
|
|
}
|
|
|
|
},
|
|
|
|
tabTargeted: {
|
|
|
|
"&::before": {
|
|
|
|
backgroundColor: '#fd6a00',
|
|
|
|
}
|
|
|
|
},
|
2020-06-29 23:43:49 +02:00
|
|
|
}));
|
|
|
|
|
2021-01-26 22:10:28 +01:00
|
|
|
const languageToJiraProject = new Map(Object.entries({
|
2020-09-25 13:11:49 +02:00
|
|
|
"PYTHON": "SONARPY",
|
|
|
|
"ABAP": "SONARABAP",
|
|
|
|
"CFAMILY": "CPP",
|
|
|
|
"JAVA": "SONARJAVA",
|
|
|
|
"COBOL": "SONARCOBOL",
|
|
|
|
"FLEX": "SONARFLEX",
|
|
|
|
"HTML": "SONARHTML",
|
|
|
|
"PHP": "SONARPHP",
|
|
|
|
"PLI": "SONARPLI",
|
|
|
|
"PLSQL": "SONARPLSQL",
|
|
|
|
"RPG": "SONARRPG",
|
|
|
|
"APEX": "SONARSLANG",
|
|
|
|
"RUBY": "SONARSLANG",
|
2021-06-24 16:28:40 +02:00
|
|
|
"KOTLIN": "SONARKT",
|
2020-09-25 13:11:49 +02:00
|
|
|
"SCALA": "SONARSLANG",
|
|
|
|
"GO": "SONARSLANG",
|
2021-07-01 14:26:02 +02:00
|
|
|
"SECRETS": "SECRETS",
|
2020-09-25 13:11:49 +02:00
|
|
|
"SWIFT": "SONARSWIFT",
|
|
|
|
"TSQL": "SONARTSQL",
|
|
|
|
"VB6": "SONARVBSIX",
|
|
|
|
"XML": "SONARXML",
|
2021-06-01 16:50:15 +02:00
|
|
|
"CLOUDFORMATION": "SONARIAC",
|
|
|
|
"TERRAFORM": "SONARIAC",
|
2021-01-26 22:10:28 +01:00
|
|
|
}));
|
2020-09-25 13:11:49 +02:00
|
|
|
|
2021-01-26 22:10:28 +01:00
|
|
|
const languageToGithubProject = new Map(Object.entries({
|
2020-09-25 13:11:49 +02:00
|
|
|
"ABAP": "sonar-abap",
|
|
|
|
"CSHARP": "sonar-dotnet",
|
|
|
|
"VBNET": "sonar-dotnet",
|
|
|
|
"JAVASCRIPT": "SonarJS",
|
|
|
|
"TYPESCRIPT": "SonarJS",
|
|
|
|
"SWIFT": "sonar-swift",
|
2021-06-24 16:28:40 +02:00
|
|
|
"KOTLIN": "sonar-kotlin",
|
2020-09-25 13:11:49 +02:00
|
|
|
"GO": "slang-enterprise",
|
|
|
|
"SCALA": "slang-enterprise",
|
|
|
|
"RUBY": "slang-enterprise",
|
|
|
|
"APEX": "slang-enterprise",
|
|
|
|
"HTML": "sonar-html",
|
|
|
|
"COBOL": "sonar-cobol",
|
|
|
|
"VB6": "sonar-vb",
|
|
|
|
"JAVA": "sonar-java",
|
|
|
|
"PLI": "sonar-pli",
|
|
|
|
"CFAMILY": "sonar-cpp",
|
|
|
|
"CSS": "sonar-css",
|
2021-06-01 16:50:15 +02:00
|
|
|
"FLEX": "sonar-flex",
|
2020-09-25 13:11:49 +02:00
|
|
|
"PHP": "sonar-php",
|
2021-06-01 16:50:15 +02:00
|
|
|
"PLSQL": "sonar-plsql",
|
|
|
|
"PYTHON": "sonar-python",
|
2020-09-25 13:11:49 +02:00
|
|
|
"RPG": "sonar-rpg",
|
2021-06-01 16:50:15 +02:00
|
|
|
"TSQL": "sonar-tsql",
|
2020-09-25 13:11:49 +02:00
|
|
|
"XML": "sonar-xml",
|
2021-06-01 16:50:15 +02:00
|
|
|
"CLOUDFORMATION": "sonar-iac",
|
|
|
|
"TERRAFORM": "sonar-iac",
|
2021-07-01 14:26:02 +02:00
|
|
|
"SECRETS": "sonar-secrets",
|
2021-01-26 22:10:28 +01:00
|
|
|
}));
|
2020-09-25 13:11:49 +02:00
|
|
|
|
2021-06-10 15:30:10 +02:00
|
|
|
function ticketsAndImplementationPRsLinks(ruleNumber: string, title: string, language?: string) {
|
|
|
|
if (language) {
|
|
|
|
const upperCaseLanguage = language.toUpperCase();
|
|
|
|
const jiraProject = languageToJiraProject.get(upperCaseLanguage);
|
|
|
|
const githubProject = languageToGithubProject.get(upperCaseLanguage);
|
2021-09-23 16:14:10 +02:00
|
|
|
const titleWihoutQuotes = title.replaceAll('"','');
|
2021-06-10 15:30:10 +02:00
|
|
|
|
|
|
|
const implementationPRsLink = (
|
|
|
|
<Link href={`https://github.com/SonarSource/${githubProject}/pulls?q=is%3Apr+"S${ruleNumber}"+OR+"RSPEC-${ruleNumber}"`}>
|
|
|
|
Implementation Pull Requests
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
|
|
|
|
if (jiraProject !== undefined) {
|
|
|
|
const ticketsLink = (
|
2021-09-23 16:14:10 +02:00
|
|
|
<Link href={`https://jira.sonarsource.com/issues/?jql=project%20%3D%20${jiraProject}%20AND%20(text%20~%20%22S${ruleNumber}%22%20OR%20text%20~%20%22RSPEC-${ruleNumber}%22%20OR%20text%20~%20"${titleWihoutQuotes}")`}>
|
2021-06-10 15:30:10 +02:00
|
|
|
Implementation tickets on Jira
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
return {ticketsLink, implementationPRsLink};
|
|
|
|
} else {
|
|
|
|
const ticketsLink = (
|
|
|
|
<Link href={`https://github.com/SonarSource/${githubProject}/issues?q=is%3Aissue+"S${ruleNumber}"+OR+"RSPEC-${ruleNumber}"`}>
|
|
|
|
Implementation issues on GitHub
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
return {ticketsLink, implementationPRsLink};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const ticketsLink = (<div>Select a language to see the implementation tickets</div>);
|
|
|
|
const implementationPRsLink = (<div>Select a language to see the implementation pull requests</div>);
|
|
|
|
return {ticketsLink, implementationPRsLink};
|
|
|
|
}
|
|
|
|
}
|
2020-06-29 23:43:49 +02:00
|
|
|
|
2021-01-26 22:10:28 +01:00
|
|
|
export function RulePage(props: any) {
|
2020-06-29 23:43:49 +02:00
|
|
|
const ruleid = props.match.params.ruleid;
|
2021-06-10 15:30:10 +02:00
|
|
|
// language can be absent
|
2020-06-29 23:43:49 +02:00
|
|
|
const language = props.match.params.language;
|
2021-06-08 18:32:38 +02:00
|
|
|
document.title = ruleid;
|
2020-06-29 23:43:49 +02:00
|
|
|
|
2020-06-30 14:19:29 +02:00
|
|
|
const history = useHistory();
|
2021-01-26 22:10:28 +01:00
|
|
|
function handleLanguageChange(event: any, lang: string) {
|
2020-06-30 14:19:29 +02:00
|
|
|
history.push(`/${ruleid}/${lang}`);
|
|
|
|
}
|
|
|
|
|
2020-06-29 23:43:49 +02:00
|
|
|
const classes = useStyles();
|
2021-06-03 16:30:50 +02:00
|
|
|
let branch = 'master'
|
2020-06-29 23:43:49 +02:00
|
|
|
|
2021-06-10 15:30:10 +02:00
|
|
|
let descUrl = process.env.PUBLIC_URL + '/rules/' + ruleid + "/" + (language ?? "default") + "-description.html";
|
|
|
|
let metadataUrl = process.env.PUBLIC_URL + '/rules/' + ruleid + "/" + (language ?? "default") + "-metadata.json";
|
2020-06-29 23:43:49 +02:00
|
|
|
|
2021-01-26 22:10:28 +01:00
|
|
|
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
|
|
|
|
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
|
2020-06-29 23:43:49 +02:00
|
|
|
|
2021-09-30 12:20:26 +02:00
|
|
|
const {ruleCoverage, allLangsRuleCoverage, isLanguageCovered} = useRuleCoverage();
|
2021-01-26 22:10:28 +01:00
|
|
|
let coverage: any = "Loading...";
|
2020-07-09 18:53:51 +02:00
|
|
|
|
2020-06-30 14:19:29 +02:00
|
|
|
let title = "Loading..."
|
2020-07-06 11:03:32 +02:00
|
|
|
let metadataJSONString;
|
2020-06-30 14:19:29 +02:00
|
|
|
let languagesTabs = null;
|
2021-05-20 11:24:59 +02:00
|
|
|
let prUrl: string | undefined = undefined;
|
2021-01-26 22:10:28 +01:00
|
|
|
if (metadataJSON && !metadataIsLoading && !metadataError) {
|
2021-05-20 11:24:59 +02:00
|
|
|
title = metadataJSON.title;
|
|
|
|
if ('prUrl' in metadataJSON) {
|
|
|
|
prUrl = metadataJSON.prUrl;
|
|
|
|
}
|
2021-06-03 16:30:50 +02:00
|
|
|
branch = metadataJSON.branch;
|
2021-05-20 11:24:59 +02:00
|
|
|
metadataJSON.all_languages.sort();
|
2021-09-30 12:20:26 +02:00
|
|
|
languagesTabs = metadataJSON.all_languages.map(lang => {
|
|
|
|
const isImplemented = isLanguageCovered(lang, metadataJSON!.allKeys);
|
|
|
|
const classNames = classes.tab + ' ' + (isImplemented ? classes.tabCovered : classes.tabTargeted);
|
|
|
|
return <Tab label={lang} value={lang} className={classNames} />;
|
|
|
|
});
|
2020-07-06 11:03:32 +02:00
|
|
|
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
|
2020-07-09 18:53:51 +02:00
|
|
|
|
2021-09-30 10:59:30 +02:00
|
|
|
const coverageMapper = (key: any, range: any) => {
|
|
|
|
if (typeof range === "string") {
|
|
|
|
return (
|
|
|
|
<li >{key}: {range}</li>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<li>Not covered for {key} anymore. Was covered from {range['since']} to {range['until']}.</li>
|
|
|
|
);
|
|
|
|
}
|
2021-06-10 15:30:10 +02:00
|
|
|
};
|
|
|
|
if (language) {
|
|
|
|
coverage = ruleCoverage(language, metadataJSON.allKeys, coverageMapper);
|
|
|
|
} else {
|
|
|
|
coverage = allLangsRuleCoverage(metadataJSON.allKeys, coverageMapper);
|
|
|
|
}
|
2020-06-29 23:43:49 +02:00
|
|
|
}
|
|
|
|
|
2021-09-22 13:49:19 +02:00
|
|
|
if (coverage !== "Not Covered") {
|
|
|
|
prUrl = undefined;
|
|
|
|
branch = 'master';
|
|
|
|
}
|
|
|
|
|
2021-06-03 16:30:50 +02:00
|
|
|
let editOnGithubUrl = 'https://github.com/SonarSource/rspec/blob/' +
|
2021-06-10 15:30:10 +02:00
|
|
|
branch + '/rules/' + ruleid + (language ? '/' + language : '');
|
2021-06-03 16:30:50 +02:00
|
|
|
|
2020-06-30 09:22:25 +02:00
|
|
|
let description = <div>Loading...</div>;
|
2021-01-26 22:10:28 +01:00
|
|
|
if (descHTML !== null && !descIsLoading && !descError) {
|
2020-07-06 11:03:32 +02:00
|
|
|
description = <div>
|
|
|
|
<div dangerouslySetInnerHTML={{__html: descHTML}}/>
|
|
|
|
<hr />
|
|
|
|
<a href={editOnGithubUrl}>Edit on Github</a><br/>
|
|
|
|
<hr />
|
|
|
|
<pre>{metadataJSONString}</pre>
|
|
|
|
</div>;
|
2020-06-30 09:22:25 +02:00
|
|
|
}
|
2021-05-20 11:24:59 +02:00
|
|
|
let prLink = <></>;
|
|
|
|
if (prUrl) {
|
|
|
|
prLink = <div><span className={classes.unimplemented}>Not implemented (see <a href={prUrl}>PR</a>)</span></div>
|
|
|
|
}
|
2021-06-10 15:30:10 +02:00
|
|
|
const ruleNumber = ruleid.substring(1);
|
2021-05-20 10:31:47 +02:00
|
|
|
|
|
|
|
const specificationPRsLink = (
|
|
|
|
<Link href={`https://github.com/SonarSource/rspec/pulls?q=is%3Apr+"S${ruleNumber}"+OR+"RSPEC-${ruleNumber}"`}>
|
|
|
|
Specification Pull Requests
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
|
2021-06-10 15:30:10 +02:00
|
|
|
const {ticketsLink, implementationPRsLink} = ticketsAndImplementationPRsLinks(ruleNumber, title, language);
|
|
|
|
const tabsValue = language ? {'value' : language} : {'value': false};
|
2020-07-09 18:53:51 +02:00
|
|
|
|
2020-06-29 23:43:49 +02:00
|
|
|
return (
|
2020-06-30 14:19:29 +02:00
|
|
|
<div>
|
2020-07-09 18:53:51 +02:00
|
|
|
<div className={classes.ruleBar}>
|
|
|
|
<Container>
|
2021-09-30 12:20:26 +02:00
|
|
|
<Typography variant="h2" classes={{root: classes.ruleid}}>
|
|
|
|
<Link className={classes.ruleidLink} component={RouterLink} to={`/${ruleid}`} underline="none">{ruleid}</Link>
|
|
|
|
</Typography>
|
|
|
|
<Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography>
|
|
|
|
<Tabs
|
|
|
|
{...tabsValue}
|
|
|
|
onChange={handleLanguageChange}
|
|
|
|
indicatorColor="primary"
|
|
|
|
textColor="primary"
|
|
|
|
centered
|
|
|
|
variant="scrollable"
|
|
|
|
scrollButtons="auto"
|
|
|
|
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
|
|
|
|
>
|
|
|
|
{languagesTabs}
|
|
|
|
</Tabs>
|
2020-07-09 18:53:51 +02:00
|
|
|
</Container>
|
|
|
|
</div>
|
2021-05-20 10:31:47 +02:00
|
|
|
|
2020-06-29 23:43:49 +02:00
|
|
|
<Container maxWidth="md">
|
2020-07-09 18:53:51 +02:00
|
|
|
<Typography variant="h3" classes={{root: classes.title}}>{title}</Typography>
|
2021-01-26 22:10:28 +01:00
|
|
|
<Box className={classes.coverage}>
|
2020-07-09 18:53:51 +02:00
|
|
|
<Typography variant="h4" >Covered Since</Typography>
|
|
|
|
<ul>
|
|
|
|
{coverage}
|
|
|
|
</ul>
|
|
|
|
</Box>
|
2020-09-25 13:11:49 +02:00
|
|
|
|
2021-01-26 22:10:28 +01:00
|
|
|
<Box className={classes.coverage}>
|
2020-09-25 13:11:49 +02:00
|
|
|
<Typography variant="h4" >Related Tickets and Pull Requests</Typography>
|
|
|
|
<ul>
|
2021-05-20 10:31:47 +02:00
|
|
|
{specificationPRsLink}
|
|
|
|
</ul>
|
|
|
|
<ul>
|
|
|
|
{implementationPRsLink}
|
2020-09-25 13:11:49 +02:00
|
|
|
</ul>
|
|
|
|
<ul>
|
2021-05-20 10:31:47 +02:00
|
|
|
{ticketsLink}
|
2020-09-25 13:11:49 +02:00
|
|
|
</ul>
|
|
|
|
</Box>
|
2021-05-20 10:31:47 +02:00
|
|
|
|
2021-01-26 22:10:28 +01:00
|
|
|
<Box>
|
2020-07-09 18:53:51 +02:00
|
|
|
<Typography variant="h4">Description</Typography>
|
|
|
|
<Typography className={classes.description}>
|
|
|
|
{description}
|
|
|
|
</Typography>
|
|
|
|
</Box>
|
2020-06-29 23:43:49 +02:00
|
|
|
</Container>
|
2020-06-30 14:19:29 +02:00
|
|
|
</div>
|
2021-05-20 10:31:47 +02:00
|
|
|
|
2020-06-29 23:43:49 +02:00
|
|
|
);
|
2020-07-06 11:03:32 +02:00
|
|
|
}
|