import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; import Box from '@material-ui/core/Box'; import Tooltip from '@material-ui/core/Tooltip'; import { createTheme, Link, ThemeProvider } from '@material-ui/core'; import Highlight from 'react-highlight'; import { Link as RouterLink, useHistory } from 'react-router-dom'; import { RULE_STATE, useRuleCoverage } from './utils/useRuleCoverage'; import { useFetch } from './utils/useFetch'; import RuleMetadata, { Version, Coverage } from './types/RuleMetadata'; import parse, { attributesToProps, domToReact, DOMNode, Element } from 'html-react-parser'; import VisibilityOffOutlinedIcon from '@material-ui/icons/VisibilityOffOutlined'; import './hljs-humanoid-light.css'; const PARAMETER_INTERNAL_MARGIN = 0.5; const useStyles = makeStyles((theme) => ({ '@global': { h1: { fontSize: '1.6rem', fontWeight: 500, marginTop: theme.spacing(3), marginBottom: theme.spacing(3) }, h2: { color: '#0B3C62', fontSize: '1.2rem' }, h3: { fontSize: '1rem', color: '#25699D' }, hr: { color: '#F9F9FB' }, '.sidebarblock': { '& .title': { marginTop: theme.spacing(2), color: '#25699D' }, '& pre': { marginLeft: '1rem', marginTop: theme.spacing(PARAMETER_INTERNAL_MARGIN), marginBottom: theme.spacing(PARAMETER_INTERNAL_MARGIN) }, '& p': { marginLeft: '1rem', marginTop: theme.spacing(PARAMETER_INTERNAL_MARGIN), marginBottom: theme.spacing(PARAMETER_INTERNAL_MARGIN) } } }, ruleBar: { borderBottom: '1px solid lightgrey', }, ruleid: { textAlign: 'center', marginTop: theme.spacing(3), marginBottom: theme.spacing(3), color: 'black' }, ruleidLink: { color: 'inherit', }, title: { textAlign: 'justify', marginTop: theme.spacing(4), marginBottom: theme.spacing(4), }, avoid: { textDecoration: 'line-through' }, coverage: { marginBottom: theme.spacing(3), }, description: { textAlign: 'justify', // marginBottom: theme.spacing(3), }, // style used to center the tabs when there too few of them to fill the container tabRoot: { justifyContent: 'center' }, tabScroller: { flexGrow: 0 }, 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', } }, coveredTab: { '&::before': { backgroundColor: RULE_STATE['covered'].color, } }, targetedTab: { '&::before': { borderColor: RULE_STATE['targeted'].color, border: '1px solid', backgroundColor: 'transparent' } }, removedTab: { '&::before': { backgroundColor: RULE_STATE['removed'].color, } }, closedTab: { '&::before': { backgroundColor: RULE_STATE['closed'].color, } }, deprecatedTab: { '&::before': { backgroundColor: RULE_STATE['deprecated'].color, } }, })); const theme = createTheme({}); type UsedStyles = ReturnType; const languageToJiraProject = new Map(Object.entries({ 'PYTHON': 'SONARPY', 'ABAP': 'SONARABAP', 'AZURE_RESOURCE_MANAGER': 'SONARIAC', 'CFAMILY': 'CPP', 'DART': 'DART', 'DOCKER': 'SONARIAC', 'JAVA': 'SONARJAVA', 'JCL': 'SONARJCL', 'COBOL': 'SONARCOBOL', 'FLEX': 'SONARFLEX', 'HTML': 'SONARHTML', 'PHP': 'SONARPHP', 'PLI': 'SONARPLI', 'PLSQL': 'PLSQL', 'RPG': 'SONARRPG', 'APEX': 'SONARAPEX', 'RUBY': 'SONARRUBY', 'RUST': 'SKUNK', 'KOTLIN': 'SONARKT', 'SCALA': 'SONARSCALA', 'GO': 'SONARGO', 'SECRETS': 'SONARTEXT', 'SWIFT': 'SONARSWIFT', 'TSQL': 'TSQL', 'VB6': 'VB6', 'XML': 'SONARXML', 'CLOUDFORMATION': 'SONARIAC', 'TERRAFORM': 'SONARIAC', 'KUBERNETES': 'SONARIAC', 'TEXT': 'SONARTEXT', 'ANSIBLE': 'SONARIAC', })); const languageToGithubProject = new Map(Object.entries({ 'ABAP': 'sonar-abap', 'AZURE_RESOURCE_MANAGER': 'sonar-iac', 'CSHARP': 'sonar-dotnet', 'DART': 'sonar-dart', 'DOCKER': 'sonar-iac', 'VBNET': 'sonar-dotnet', 'JAVASCRIPT': 'SonarJS', 'TYPESCRIPT': 'SonarJS', 'SWIFT': 'sonar-swift', 'KOTLIN': 'sonar-kotlin', 'GO': 'sonar-go', 'SCALA': 'sonar-scala', 'RUBY': 'sonar-ruby', 'RUST': 'sonar-rust', 'APEX': 'sonar-apex', 'HTML': 'sonar-html', 'COBOL': 'sonar-cobol', 'VB6': 'sonar-vb', 'JAVA': 'sonar-java', 'JCL': 'sonar-jcl', 'PLI': 'sonar-pli', 'CFAMILY': 'sonar-cpp', 'CSS': 'sonar-css', 'FLEX': 'sonar-flex', 'PHP': 'sonar-php', 'PLSQL': 'sonar-plsql', 'PYTHON': 'sonar-python', 'RPG': 'sonar-rpg', 'TSQL': 'sonar-tsql', 'XML': 'sonar-xml', 'CLOUDFORMATION': 'sonar-iac', 'TERRAFORM': 'sonar-iac', 'KUBERNETES': 'sonar-iac', 'SECRETS': 'sonar-text', 'TEXT': 'sonar-text', 'ANSIBLE': 'sonar-iac-enterprise', })); function ticketsAndImplementationPRsLinks(ruleNumber: string, title: string, language?: string) { if (language) { const upperCaseLanguage = language.toUpperCase(); const jiraProject = languageToJiraProject.get(upperCaseLanguage); const githubProject = languageToGithubProject.get(upperCaseLanguage); const titleWihoutQuotes = title.replace(/"/g, "'"); const implementationPRsLink = ( Implementation Pull Requests ); if (jiraProject !== undefined) { const ticketsLink = ( Implementation tickets on Jira ); return {ticketsLink, implementationPRsLink}; } else { const ticketsLink = ( Implementation issues on GitHub ); return {ticketsLink, implementationPRsLink}; } } else { const ticketsLink = (
Select a language to see the implementation tickets
); const implementationPRsLink = (
Select a language to see the implementation pull requests
); return {ticketsLink, implementationPRsLink}; } } const RuleThemeProvider: React.FC = ({ children }) => { useStyles(); return {children}; } interface PageMetadata { title: string; languagesTabs: JSX.Element[] | null; avoid: boolean; prUrl: string | undefined; branch: string; coverage: Coverage; isInQualityProfile: boolean; jsonString: string | undefined; } function usePageMetadata(ruleid: string, language: string, classes: UsedStyles): PageMetadata { const metadataUrl = `${process.env.PUBLIC_URL}/rules/${ruleid}/${language ?? 'default'}-metadata.json`; let [metadataJSON, metadataError, metadataIsLoading] = useFetch(metadataUrl); let coverage: Coverage = 'Loading...'; let title = 'Loading...'; let avoid = false; let isInQualityProfile = false; let metadataJSONString; let languagesTabs = null; let prUrl: string | undefined = undefined; let branch = 'master'; const { ruleCoverage, allLangsRuleCoverage, ruleStateInAnalyzer } = useRuleCoverage(); if (metadataJSON && !metadataIsLoading && !metadataError) { title = metadataJSON.title; if ('prUrl' in metadataJSON) { prUrl = metadataJSON.prUrl; } branch = metadataJSON.branch; metadataJSON.languagesSupport.sort((a, b) => a.name.localeCompare(b.name)); const ruleStates = metadataJSON.languagesSupport.map(({ name, status }) => ({ name, ruleState: ruleStateInAnalyzer(name, metadataJSON!.allKeys, status) })); languagesTabs = ruleStates.map(({ name, ruleState }) => { const classNames = classes.tab + ' ' + (classes as any)[ruleState + 'Tab']; return ; }); avoid = !ruleStates.some(({ ruleState }) => ruleState === 'covered' || ruleState === 'targeted'); metadataJSONString = JSON.stringify(metadataJSON, null, 2); const coverageMapper = (key: string, range: Version ): JSX.Element => { if (typeof range === 'string') { return (
  • {key}: {range}
  • ); } else { return (
  • Not covered for {key} anymore. Was covered from {range.since} to {range.until}.
  • ); } }; if (language) { coverage = ruleCoverage(language, metadataJSON.allKeys, coverageMapper); } else { coverage = allLangsRuleCoverage(metadataJSON.allKeys, coverageMapper); } isInQualityProfile = metadataJSON.defaultQualityProfiles.length > 0; } if (coverage !== 'Not Covered') { prUrl = undefined; branch = 'master'; } return { title, languagesTabs, avoid, prUrl, branch, coverage, isInQualityProfile, jsonString: metadataJSONString }; } function getRspecPath(rspecId: string, language?: string) { return '/rspec#/rspec/' + rspecId; } function useDescription(metadata: PageMetadata, ruleid: string, language?: string) { const editOnGithubUrl = `https://github.com/SonarSource/rspec/blob/${metadata.branch}/rules/${ruleid}${language ? '/' + language : ''}`; function htmlReplacement(domNode: Element) { if (domNode.name === 'a' && domNode.attribs?.['data-rspec-id']) { const props = attributesToProps(domNode.attribs); return {domToReact(domNode.children)} ; } if (domNode.name === 'code' && domNode.attribs?.['data-lang']) { return {domToReact(domNode.children)} ; } return undefined; // No modification. } const descUrl = `${process.env.PUBLIC_URL}/rules/${ruleid}/${language ?? 'default'}-description.html`; const [descHTML, descError, descIsLoading] = useFetch(descUrl, false); if (descHTML !== null && !descIsLoading && !descError) { return
    {parse(descHTML, { replace: (d: DOMNode) => htmlReplacement(d as Element) })}
    Edit on Github

    {metadata.jsonString}
    ; } return
    Loading...
    ; } export function RulePage(props: any) { // language can be absent const {ruleid, language} = props.match.params; document.title = ruleid; const history = useHistory(); function handleLanguageChange(event: any, lang: string) { history.push(`/${ruleid}/${lang}`); } const classes = useStyles(); const metadata = usePageMetadata(ruleid, language, classes); const description = useDescription(metadata, ruleid, language); let prLink = <>; if (metadata.prUrl) { prLink =
    Not implemented (see PR)
    ; } const ruleNumber = ruleid.substring(1); const specificationPRsLink = ( Specification Pull Requests ); const {ticketsLink, implementationPRsLink} = ticketsAndImplementationPRsLinks(ruleNumber, metadata.title, language); const tabsValue = language ? {'value' : language} : {'value': false}; return (
    {ruleid} {prLink} {metadata.languagesTabs}

    {metadata.isInQualityProfile ? <> : <> } {metadata.title}


    Covered Since

      {metadata.coverage}

    Related Tickets and Pull Requests

      {specificationPRsLink}
      {implementationPRsLink}
      {ticketsLink}
    {description}
    ); }