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', () => { test('renders see the GH PR link', () => {
const { getByText } = render(<App />); const { getByText } = render(<App />);
const linkElement = getByText(/Unimplemented rules/i); const linkElement = getByText(/Rules under specification/i);
expect(linkElement).toBeInTheDocument(); 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 Tab from '@material-ui/core/Tab';
import Box from '@material-ui/core/Box'; import Box from '@material-ui/core/Box';
import { Link } from '@material-ui/core'; import { Link } from '@material-ui/core';
import { Link as RouterLink } from 'react-router-dom';
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@ -24,6 +25,9 @@ const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(3), marginTop: theme.spacing(3),
marginBottom: theme.spacing(3), marginBottom: theme.spacing(3),
}, },
ruleidLink: {
color: 'inherit',
},
title: { title: {
textAlign: 'justify', textAlign: 'justify',
marginTop: theme.spacing(4), marginTop: theme.spacing(4),
@ -46,7 +50,34 @@ const useStyles = makeStyles((theme) => ({
}, },
unimplemented: { unimplemented: {
color: 'red' 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({ const languageToJiraProject = new Map(Object.entries({
@ -161,7 +192,7 @@ export function RulePage(props: any) {
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false); let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl); let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
const {ruleCoverage, allLangsRuleCoverage} = useRuleCoverage(); const {ruleCoverage, allLangsRuleCoverage, isLanguageCovered} = useRuleCoverage();
let coverage: any = "Loading..."; let coverage: any = "Loading...";
let title = "Loading..." let title = "Loading..."
@ -175,7 +206,11 @@ export function RulePage(props: any) {
} }
branch = metadataJSON.branch; branch = metadataJSON.branch;
metadataJSON.all_languages.sort(); 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); metadataJSONString = JSON.stringify(metadataJSON, null, 2);
const coverageMapper = (key: any, range: any) => { const coverageMapper = (key: any, range: any) => {
@ -233,20 +268,22 @@ export function RulePage(props: any) {
<div> <div>
<div className={classes.ruleBar}> <div className={classes.ruleBar}>
<Container> <Container>
<Typography variant="h2" classes={{root: classes.ruleid}}>{ruleid}</Typography> <Typography variant="h2" classes={{root: classes.ruleid}}>
<Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography> <Link className={classes.ruleidLink} component={RouterLink} to={`/${ruleid}`} underline="none">{ruleid}</Link>
<Tabs </Typography>
{...tabsValue} <Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography>
onChange={handleLanguageChange} <Tabs
indicatorColor="primary" {...tabsValue}
textColor="primary" onChange={handleLanguageChange}
centered indicatorColor="primary"
variant="scrollable" textColor="primary"
scrollButtons="auto" centered
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }} variant="scrollable"
> scrollButtons="auto"
{languagesTabs} classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
</Tabs> >
{languagesTabs}
</Tabs>
</Container> </Container>
</div> </div>

View File

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

View File

@ -227,9 +227,6 @@ export const SearchPage = () => {
<Box className={classes.resultsCount}> <Box className={classes.resultsCount}>
<Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography> <Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography>
</Box> </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> </Box>
{resultsDisplay} {resultsDisplay}
<Pagination count={totalPages} page={pageNumber} siblingCount={2} <Pagination count={totalPages} page={pageNumber} siblingCount={2}

View File

@ -21,7 +21,7 @@ export default function TopBar() {
SonarSource Rule Specifications SonarSource Rule Specifications
</Typography> </Typography>
<Button href="https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22"> <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> </Button>
</Toolbar> </Toolbar>
</AppBar> </AppBar>

View File

@ -2,51 +2,42 @@ import { useFetch } from './useFetch';
type RuleCoverage = Record<string, Record<string, string>>; 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 coveredRulesUrl = `${process.env.PUBLIC_URL}/covered_rules.json`;
const [coveredRules, coveredRulesError, coveredRulesIsLoading] = useFetch<RuleCoverage>(coveredRulesUrl); 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() { function ruleCoverageForSonarpediaKeys(languageKeys: string[], ruleKeys: string[], mapper: any) {
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) {
if (coveredRulesError) { if (coveredRulesError) {
return 'Failed Loading'; return 'Failed Loading';
} }
@ -57,11 +48,10 @@ export function useRuleCoverage() {
throw new Error('coveredRules is empty'); throw new Error('coveredRules is empty');
} }
const result: any[] = []; const result: any[] = [];
sonarpediaKeys.forEach(sonarpediaKey => { languageKeys.forEach(language => {
ruleKeys.forEach(ruleKey => { ruleKeys.forEach(ruleKey => {
if (sonarpediaKey in coveredRules && if (language in coveredRules && ruleKey in coveredRules[language]) {
ruleKey in coveredRules[sonarpediaKey]) { result.push(mapper(language, coveredRules[language][ruleKey]))
result.push(mapper(sonarpediaKey, coveredRules[sonarpediaKey][ruleKey]))
} }
}); });
}); });
@ -81,8 +71,25 @@ export function useRuleCoverage() {
} }
function allLangsRuleCoverage(ruleKeys: string[], mapper: any) { function allLangsRuleCoverage(ruleKeys: string[], mapper: any) {
const allLanguageKeys = Array.from(languageToSonarpedia.values()).flat();
return ruleCoverageForSonarpediaKeys(allLanguageKeys, ruleKeys, mapper); 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};
} }