RULEAPI-688 Display covered languages on search page

This commit is contained in:
Elena Vilchik 2021-09-30 12:20:26 +02:00 committed by GitHub
parent 6d8404981c
commit 1d44a68991
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 166 additions and 93 deletions

View File

@ -4,8 +4,6 @@ import App from './App';
test('renders see the GH PR link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/Unimplemented rules/i);
const linkElement = getByText(/Rules under specification/i);
expect(linkElement).toBeInTheDocument();
const searchLinkElement = getByText(/Search in unimplemented/i);
expect(searchLinkElement).toBeInTheDocument();
});

View File

@ -7,6 +7,7 @@ import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Box from '@material-ui/core/Box';
import { Link } from '@material-ui/core';
import { Link as RouterLink } from 'react-router-dom';
import { useHistory } from "react-router-dom";
@ -24,6 +25,9 @@ const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
},
ruleidLink: {
color: 'inherit',
},
title: {
textAlign: 'justify',
marginTop: theme.spacing(4),
@ -46,7 +50,34 @@ const useStyles = makeStyles((theme) => ({
},
unimplemented: {
color: 'red'
}
},
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',
}
},
}));
const languageToJiraProject = new Map(Object.entries({
@ -161,7 +192,7 @@ export function RulePage(props: any) {
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
const {ruleCoverage, allLangsRuleCoverage} = useRuleCoverage();
const {ruleCoverage, allLangsRuleCoverage, isLanguageCovered} = useRuleCoverage();
let coverage: any = "Loading...";
let title = "Loading..."
@ -175,7 +206,11 @@ export function RulePage(props: any) {
}
branch = metadataJSON.branch;
metadataJSON.all_languages.sort();
languagesTabs = metadataJSON.all_languages.map(lang => <Tab label={lang} value={lang}/>);
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} />;
});
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
const coverageMapper = (key: any, range: any) => {
@ -233,20 +268,22 @@ export function RulePage(props: any) {
<div>
<div className={classes.ruleBar}>
<Container>
<Typography variant="h2" classes={{root: classes.ruleid}}>{ruleid}</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>
<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>
</Container>
</div>

View File

@ -9,7 +9,12 @@ import Chip from '@material-ui/core/Chip';
import { Link as RouterLink } from 'react-router-dom';
import { Link } from '@material-ui/core';
import { IndexedRule } from './types/IndexStore';
import { useRuleCoverage } from './utils/useRuleCoverage';
const blue = '#4c9bd6';
const darkerBlue = '#25699d';
const orange = '#fd6a00';
const darkerOrange = '#c45200';
const useStyles = makeStyles((theme) => ({
searchHit: {
@ -24,18 +29,33 @@ const useStyles = makeStyles((theme) => ({
marginRight: theme.spacing(1),
marginTop: theme.spacing(2),
},
languageChip: {
coveredLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(2),
backgroundColor: '#4c9bd6',
backgroundColor: blue,
'&:hover, &:focus': {
backgroundColor: '#25699D'
backgroundColor: darkerBlue
},
},
unimplementedMarker: {
targetedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(2),
backgroundColor: '#fd6a00'
backgroundColor: orange,
'&:hover, &:focus': {
backgroundColor: darkerOrange
},
},
targetedMarker: {
marginTop: theme.spacing(2),
marginRight: theme.spacing(2),
borderColor: orange,
color: orange
},
coveredMarker: {
marginTop: theme.spacing(2),
marginRight: theme.spacing(2),
borderColor: blue,
color: blue
}
}));
@ -44,27 +64,43 @@ type SearchHitProps = {
}
export function SearchHit(props: SearchHitProps) {
const { isLanguageCovered } = useRuleCoverage();
const classes = useStyles();
const coveredLanguages: JSX.Element[] = [];
const targetedLanguages: JSX.Element[] = [];
const actualLanguages = props.data.languages.filter(language => language !== 'default');
const languages = actualLanguages.map(lang => (
<Link component={RouterLink} to={`/${props.data.id}/${lang}`} style={{ textDecoration: 'none' }}>
<Chip
classes={{root: classes.languageChip}}
label={lang}
color="primary"
clickable
/>
</Link>
));
actualLanguages.forEach(lang => {
const covered = isLanguageCovered(lang, props.data.all_keys);
const chip = <Link component={RouterLink} to={`/${props.data.id}/${lang}`} style={{ textDecoration: 'none' }}>
<Chip
classes={{root: covered ? classes.coveredLanguageChip : classes.targetedLanguageChip }}
label={lang}
color="primary"
clickable
/>
</Link>;
(covered ? coveredLanguages : targetedLanguages).push(chip);
});
const titles = props.data.titles.map(title => (
<Typography variant="body1" component="p" gutterBottom>
{title}
</Typography>
));
let unimplementedMarker = <></>;
if (props.data.prUrl) {
unimplementedMarker = <Chip classes={{root: classes.unimplementedMarker}} label="Not implemented" color="secondary" />
}
const coveredBlock = coveredLanguages.length === 0 ? <></>
: <Typography variant="body2" component="p" classes={{root: classes.language}}>
<Chip classes={{root: classes.coveredMarker}} label="Covered" color="primary" variant="outlined" />
{coveredLanguages}
</Typography>;
const targetedBlock = targetedLanguages.length === 0 ? <></>
:<Typography variant="body2" component="p" classes={{root: classes.language}}>
<Chip classes={{root: classes.targetedMarker}} label="Targeted" color="secondary" variant="outlined" />
{targetedLanguages}
</Typography>;
return (
<Card variant="outlined" classes={{root: classes.searchHit}}>
<CardContent>
@ -72,12 +108,10 @@ export function SearchHit(props: SearchHitProps) {
<Link component={RouterLink} to={`/${props.data.id}`}>
<div> Rule {props.data.id} </div>
</Link>
{unimplementedMarker}
</Typography>
{titles}
<Typography variant="body2" component="p" classes={{root: classes.language}}>
{languages}
</Typography>
{coveredBlock}
{targetedBlock}
</CardContent>
</Card>
)

View File

@ -227,9 +227,6 @@ export const SearchPage = () => {
<Box className={classes.resultsCount}>
<Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography>
</Box>
<Typography variant="subtitle1">
<a href={"https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22+" + query}>Search in unimplemented</a>
</Typography>
</Box>
{resultsDisplay}
<Pagination count={totalPages} page={pageNumber} siblingCount={2}

View File

@ -21,7 +21,7 @@ export default function TopBar() {
SonarSource Rule Specifications
</Typography>
<Button href="https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22">
<span className={classes.unimplemented} > Unimplemented rules </span>
<span className={classes.unimplemented} > Rules under specification </span>
</Button>
</Toolbar>
</AppBar>

View File

@ -2,51 +2,42 @@ import { useFetch } from './useFetch';
type RuleCoverage = Record<string, Record<string, string>>;
export function useRuleCoverage() {
const languageToSonarpedia = new Map<string, string[]>(Object.entries({
'abap': ['ABAP'],
'apex': ['APEX'],
'cfamily': ['CPP', 'C', 'OBJC'],
'cobol': ['COBOL'],
'csharp': ['CSH'],
'vbnet': ['VBNET'],
'css': ['CSS'],
'flex': ['FLEX'],
'kotlin': ['KOTLIN'],
'scala': ['SCALA'],
'ruby': ['RUBY'],
'go': ['GO'],
'java': ['JAVA'],
'javascript': ['JAVASCRIPT', 'JS', 'TYPESCRIPT'],
'php': ['PHP'],
'pli': ['PLI'],
'plsql': ['PLSQL'],
'python': ['PY'],
'rpg': ['RPG'],
'secrets': ['SECRETS'],
'swift': ['SWIFT'],
'tsql': ['TSQL'],
'vb6': ['VB'],
'WEB': ['WEB'],
'xml': ['XML'],
'html': ['HTML'],
'cloudformation': ['CLOUDFORMATION'],
'terraform': ['TERRAFORM']
}));
export function useRuleCoverage() {
const coveredRulesUrl = `${process.env.PUBLIC_URL}/covered_rules.json`;
const [coveredRules, coveredRulesError, coveredRulesIsLoading] = useFetch<RuleCoverage>(coveredRulesUrl);
const languageToSonarpedia = new Map<string, string[]>(Object.entries({
'abap': ['ABAP'],
'apex': ['APEX'],
'cfamily': ['CPP', 'C', 'OBJC'],
'cobol': ['COBOL'],
'csharp': ['CSH'],
'vbnet': ['VBNET'],
'css': ['CSS'],
'flex': ['FLEX'],
'kotlin': ['KOTLIN'],
'scala': ['SCALA'],
'ruby': ['RUBY'],
'go': ['GO'],
'java': ['JAVA'],
'javascript': ['JAVASCRIPT', 'JS', 'TYPESCRIPT'],
'php': ['PHP'],
'pli': ['PLI'],
'plsql': ['PLSQL'],
'python': ['PY'],
'rpg': ['RPG'],
'secrets': ['SECRETS'],
'swift': ['SWIFT'],
'tsql': ['TSQL'],
'vb6': ['VB'],
'WEB': ['WEB'],
'xml': ['XML'],
'html': ['HTML'],
'cloudformation': ['CLOUDFORMATION'],
'terraform': ['TERRAFORM']
}));
const allLanguageKeys = collectAllLanguageKeys();
function collectAllLanguageKeys() {
let ret = new Set<string>();
languageToSonarpedia.forEach((sonarpediaKeys, lang) => {
sonarpediaKeys.forEach(key => ret.add(key));
});
return Array.from(ret);
}
function ruleCoverageForSonarpediaKeys(sonarpediaKeys: string[], ruleKeys: string[], mapper: any) {
function ruleCoverageForSonarpediaKeys(languageKeys: string[], ruleKeys: string[], mapper: any) {
if (coveredRulesError) {
return 'Failed Loading';
}
@ -57,11 +48,10 @@ export function useRuleCoverage() {
throw new Error('coveredRules is empty');
}
const result: any[] = [];
sonarpediaKeys.forEach(sonarpediaKey => {
languageKeys.forEach(language => {
ruleKeys.forEach(ruleKey => {
if (sonarpediaKey in coveredRules &&
ruleKey in coveredRules[sonarpediaKey]) {
result.push(mapper(sonarpediaKey, coveredRules[sonarpediaKey][ruleKey]))
if (language in coveredRules && ruleKey in coveredRules[language]) {
result.push(mapper(language, coveredRules[language][ruleKey]))
}
});
});
@ -81,8 +71,25 @@ export function useRuleCoverage() {
}
function allLangsRuleCoverage(ruleKeys: string[], mapper: any) {
const allLanguageKeys = Array.from(languageToSonarpedia.values()).flat();
return ruleCoverageForSonarpediaKeys(allLanguageKeys, ruleKeys, mapper);
}
return {ruleCoverage, allLangsRuleCoverage};
function isLanguageCovered(language: string, ruleKeys: string[]): boolean {
const languageKeys = languageToSonarpedia.get(language);
if (!languageKeys) {
return false;
}
if (coveredRulesError || coveredRulesIsLoading) {
return false;
}
if (!coveredRules) {
throw new Error('coveredRules is empty');
}
return !!languageKeys.find(lang =>
ruleKeys.find(ruleKey => lang in coveredRules && ruleKey in coveredRules[lang])
);
}
return {ruleCoverage, allLangsRuleCoverage, isLanguageCovered};
}