RULEAPI-705 differentiate closed and obsolete rules in the UI (#704)

This commit is contained in:
Fred Tingaud 2022-01-17 20:13:15 +01:00 committed by GitHub
parent 26267dcbf0
commit c8c825feb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2359 additions and 424 deletions

View File

@ -7,3 +7,4 @@ sonar.tests=src
sonar.test.inclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
sonar.javascript.coveragePlugin=lcov
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.cpd.exclusions=src/deployment/__tests__/resources/**

View File

@ -83,65 +83,75 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: RULE_STATE['removed'].color,
}
},
closedTab: {
'&::before': {
backgroundColor: RULE_STATE['closed'].color,
}
},
deprecatedTab: {
'&::before': {
backgroundColor: RULE_STATE['deprecated'].color,
}
},
}));
const languageToJiraProject = new Map(Object.entries({
"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",
"KOTLIN": "SONARKT",
"SCALA": "SONARSLANG",
"GO": "SONARSLANG",
"SECRETS": "SECRETS",
"SWIFT": "SONARSWIFT",
"TSQL": "SONARTSQL",
"VB6": "SONARVBSIX",
"XML": "SONARXML",
"CLOUDFORMATION": "SONARIAC",
"TERRAFORM": "SONARIAC",
"TEXT": "SONARTEXT",
'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',
'KOTLIN': 'SONARKT',
'SCALA': 'SONARSLANG',
'GO': 'SONARSLANG',
'SECRETS': 'SECRETS',
'SWIFT': 'SONARSWIFT',
'TSQL': 'SONARTSQL',
'VB6': 'SONARVBSIX',
'XML': 'SONARXML',
'CLOUDFORMATION': 'SONARIAC',
'TERRAFORM': 'SONARIAC',
'TEXT': 'SONARTEXT',
}));
const languageToGithubProject = new Map(Object.entries({
"ABAP": "sonar-abap",
"CSHARP": "sonar-dotnet",
"VBNET": "sonar-dotnet",
"JAVASCRIPT": "SonarJS",
"TYPESCRIPT": "SonarJS",
"SWIFT": "sonar-swift",
"KOTLIN": "sonar-kotlin",
"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",
"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",
"SECRETS": "sonar-secrets",
"TEXT": "sonar-text",
'ABAP': 'sonar-abap',
'CSHARP': 'sonar-dotnet',
'VBNET': 'sonar-dotnet',
'JAVASCRIPT': 'SonarJS',
'TYPESCRIPT': 'SonarJS',
'SWIFT': 'sonar-swift',
'KOTLIN': 'sonar-kotlin',
'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',
'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',
'SECRETS': 'sonar-secrets',
'TEXT': 'sonar-text',
}));
function ticketsAndImplementationPRsLinks(ruleNumber: string, title: string, language?: string) {
@ -193,16 +203,16 @@ export function RulePage(props: any) {
const classes = useStyles();
let branch = 'master'
let descUrl = process.env.PUBLIC_URL + '/rules/' + ruleid + "/" + (language ?? "default") + "-description.html";
let metadataUrl = process.env.PUBLIC_URL + '/rules/' + ruleid + "/" + (language ?? "default") + "-metadata.json";
const descUrl = `${process.env.PUBLIC_URL}/rules/${ruleid}/${language ?? 'default'}-description.html`;
const metadataUrl = `${process.env.PUBLIC_URL}/rules/${ruleid}/${language ?? 'default'}-metadata.json`;
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
const {ruleCoverage, allLangsRuleCoverage, ruleStateInAnalyzer} = useRuleCoverage();
let coverage: any = "Loading...";
let coverage: any = 'Loading...';
let title = "Loading..."
let title = 'Loading...';
let metadataJSONString;
let languagesTabs = null;
let prUrl: string | undefined = undefined;
@ -212,16 +222,16 @@ export function RulePage(props: any) {
prUrl = metadataJSON.prUrl;
}
branch = metadataJSON.branch;
metadataJSON.all_languages.sort();
languagesTabs = metadataJSON.all_languages.map(lang => {
const ruleState = ruleStateInAnalyzer(lang, metadataJSON!.allKeys);
metadataJSON.languagesSupport.sort();
languagesTabs = metadataJSON.languagesSupport.map(({ name, status }) => {
const ruleState = ruleStateInAnalyzer(name, metadataJSON!.allKeys, status);
const classNames = classes.tab + ' ' + (classes as any)[ruleState + 'Tab'];
return <Tab key={lang} label={lang} value={lang} className={classNames} />;
return <Tab key={name} label={name} value={name} className={classNames} />;
});
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
const coverageMapper = (key: any, range: any) => {
if (typeof range === "string") {
if (typeof range === 'string') {
return (
<li key={key} >{key}: {range}</li>
);
@ -238,7 +248,7 @@ export function RulePage(props: any) {
}
}
if (coverage !== "Not Covered") {
if (coverage !== 'Not Covered') {
prUrl = undefined;
branch = 'master';
}

View File

@ -48,6 +48,22 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: RULE_STATE['removed'].darker
},
},
deprecatedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(2),
backgroundColor: RULE_STATE['deprecated'].color,
'&:hover, &:focus': {
backgroundColor: RULE_STATE['deprecated'].darker
},
},
closedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(2),
backgroundColor: RULE_STATE['closed'].color,
'&:hover, &:focus': {
backgroundColor: RULE_STATE['closed'].darker
},
},
targetedMarker: {
marginTop: theme.spacing(2),
marginRight: theme.spacing(2),
@ -65,6 +81,18 @@ const useStyles = makeStyles((theme) => ({
marginRight: theme.spacing(2),
borderColor: RULE_STATE['removed'].color,
color: RULE_STATE['removed'].color
},
deprecatedMarker: {
marginTop: theme.spacing(2),
marginRight: theme.spacing(2),
borderColor: RULE_STATE['deprecated'].color,
color: RULE_STATE['deprecated'].color
},
closedMarker: {
marginTop: theme.spacing(2),
marginRight: theme.spacing(2),
borderColor: RULE_STATE['closed'].color,
color: RULE_STATE['closed'].color
}
}));
@ -79,26 +107,39 @@ export function SearchHit(props: SearchHitProps) {
const coveredLanguages: JSX.Element[] = [];
const targetedLanguages: JSX.Element[] = [];
const removedLanguages: JSX.Element[] = [];
const deprecatedLanguages: JSX.Element[] = [];
const closedLanguages: JSX.Element[] = [];
const actualLanguages = props.data.languages.filter(language => language !== 'default');
const actualLanguages = props.data.languages.filter(l => l.name !== 'default');
actualLanguages.forEach(lang => {
const ruleState = ruleStateInAnalyzer(lang, props.data.all_keys);
const chip = <Link key={lang} component={RouterLink} to={`/${props.data.id}/${lang}`}
const ruleState = ruleStateInAnalyzer(lang.name, props.data.all_keys, lang.status);
const chip = <Link key={lang.name} component={RouterLink} to={`/${props.data.id}/${lang.name}`}
style={{ textDecoration: 'none' }}>
<Chip
classes={{root: (classes as any)[ruleState + 'LanguageChip']}}
label={lang}
label={lang.name}
color="primary"
clickable
key="{lang}"
/>
</Link>;
if (ruleState === 'covered') {
coveredLanguages.push(chip);
} else if (ruleState === 'targeted') {
switch(ruleState) {
case 'targeted':
targetedLanguages.push(chip);
} else {
break;
case 'removed':
removedLanguages.push(chip);
break;
case 'deprecated':
deprecatedLanguages.push(chip);
break;
case 'closed':
closedLanguages.push(chip);
break;
case 'covered':
default:
coveredLanguages.push(chip);
break;
}
});
const titles = props.data.titles.map(title => (
@ -125,6 +166,17 @@ export function SearchHit(props: SearchHitProps) {
{removedLanguages}
</Typography>;
const deprecatedBlock = deprecatedLanguages.length === 0 ? <></>
: <Typography key="closed-marker" variant="body2" component="div" classes={{ root: classes.language }}>
<Chip classes={{ root: classes.deprecatedMarker }} label="Deprecated" color="secondary" variant="outlined" />
{deprecatedLanguages}
</Typography>;
const closedBlock = closedLanguages.length === 0 ? <></>
: <Typography key="deprecated-marker" variant="body2" component="div" classes={{ root: classes.language }}>
<Chip classes={{ root: classes.closedMarker }} label="Closed" color="secondary" variant="outlined" />
{closedLanguages}
</Typography>;
return (
<Card variant="outlined" classes={{root: classes.searchHit}}>
<CardContent>
@ -137,6 +189,8 @@ export function SearchHit(props: SearchHitProps) {
{coveredBlock}
{targetedBlock}
{removedBlock}
{deprecatedBlock}
{closedBlock}
</CardContent>
</Card>
)

View File

@ -7,7 +7,7 @@ import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { fetchMock } from '../testutils'
const rulesPath = path.join(__dirname, '..', 'deployment', '__tests__', 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, '..', 'deployment', '__tests__', 'resources', 'metadata');
function readRuleFile(ruleId, filename) {
return fs.readFileSync(path.join(rulesPath, ruleId, filename)).toString();

View File

@ -17,7 +17,7 @@ function normalize(obj) {
}
beforeEach(() => {
const rulePath = path.join(__dirname, '..', 'deployment', '__tests__', 'resources', 'plugin_rules');
const rulePath = path.join(__dirname, '..', 'deployment', '__tests__', 'resources', 'metadata');
const [indexStore, indexAggregates] = buildIndexStore(rulePath);
const searchIndex = buildSearchIndex(indexStore);
const rootUrl = process.env.PUBLIC_URL;
@ -26,8 +26,9 @@ beforeEach(() => {
mockUrls[`${rootUrl}/rules/rule-index-store.json`] = {json: normalize(indexStore)};
mockUrls[`${rootUrl}/rules/rule-index-aggregates.json`] = {json: normalize(indexAggregates)};
mockUrls[`${rootUrl}/covered_rules.json`] = {json:
{'ABAP': {'S100': 'ver1', 'S200': 'ver2'},
'C': {'S100': 'c1', 'S234': {'since': 'c2', 'until': 'c3'}}}
{'CPP': {'S1000': 'ver1', 'S987': 'ver2', 'S3457': 'ver1'},
'C': {'S1000': 'c1', 'S234': {'since': 'c2', 'until': 'c3'}},
'PY': {'S3457': {'since': 'p2', 'until': 'p3'}}}
};
jest.spyOn(global, 'fetch').mockImplementation(fetchMock(mockUrls));
});

File diff suppressed because it is too large Load Diff

View File

@ -341,12 +341,12 @@ exports[`renders the list of all rules 1`] = `
class="MuiTypography-root makeStyles-language-16 MuiTypography-body2"
>
<div
class="MuiChip-root makeStyles-targetedMarker-20 MuiChip-colorSecondary MuiChip-outlined MuiChip-outlinedSecondary"
class="MuiChip-root makeStyles-coveredMarker-23 MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
>
<span
class="MuiChip-label"
>
Targeted
Covered
</span>
</div>
<a
@ -355,7 +355,7 @@ exports[`renders the list of all rules 1`] = `
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-18 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
class="MuiButtonBase-root MuiChip-root makeStyles-coveredLanguageChip-17 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
@ -374,7 +374,7 @@ exports[`renders the list of all rules 1`] = `
</div>
</div>
<div
class="MuiBox-root MuiBox-root-23 makeStyles-searchHitBox-2"
class="MuiBox-root MuiBox-root-27 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-14 MuiPaper-outlined MuiPaper-rounded"
@ -413,12 +413,12 @@ exports[`renders the list of all rules 1`] = `
class="MuiTypography-root makeStyles-language-16 MuiTypography-body2"
>
<div
class="MuiChip-root makeStyles-targetedMarker-20 MuiChip-colorSecondary MuiChip-outlined MuiChip-outlinedSecondary"
class="MuiChip-root makeStyles-coveredMarker-23 MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
>
<span
class="MuiChip-label"
>
Targeted
Covered
</span>
</div>
<a
@ -427,7 +427,7 @@ exports[`renders the list of all rules 1`] = `
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-18 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
class="MuiButtonBase-root MuiChip-root makeStyles-coveredLanguageChip-17 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
@ -441,6 +441,19 @@ exports[`renders the list of all rules 1`] = `
/>
</div>
</a>
</div>
<div
class="MuiTypography-root makeStyles-language-16 MuiTypography-body2"
>
<div
class="MuiChip-root makeStyles-targetedMarker-22 MuiChip-colorSecondary MuiChip-outlined MuiChip-outlinedSecondary"
>
<span
class="MuiChip-label"
>
Targeted
</span>
</div>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/csharp"
@ -461,33 +474,26 @@ exports[`renders the list of all rules 1`] = `
/>
</div>
</a>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/java"
style="text-decoration: none;"
</div>
<div
class="MuiTypography-root makeStyles-language-16 MuiTypography-body2"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-18 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
class="MuiChip-root makeStyles-removedMarker-24 MuiChip-colorSecondary MuiChip-outlined MuiChip-outlinedSecondary"
>
<span
class="MuiChip-label"
>
java
Removed
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/python"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-18 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
class="MuiButtonBase-root MuiChip-root makeStyles-removedLanguageChip-19 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
@ -502,11 +508,44 @@ exports[`renders the list of all rules 1`] = `
</div>
</a>
</div>
<div
class="MuiTypography-root makeStyles-language-16 MuiTypography-body2"
>
<div
class="MuiChip-root makeStyles-closedMarker-26 MuiChip-colorSecondary MuiChip-outlined MuiChip-outlinedSecondary"
>
<span
class="MuiChip-label"
>
Closed
</span>
</div>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/java"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-closedLanguageChip-21 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
java
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</div>
</div>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-24 makeStyles-searchHitBox-2"
class="MuiBox-root MuiBox-root-28 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-14 MuiPaper-outlined MuiPaper-rounded"
@ -535,12 +574,12 @@ exports[`renders the list of all rules 1`] = `
class="MuiTypography-root makeStyles-language-16 MuiTypography-body2"
>
<div
class="MuiChip-root makeStyles-targetedMarker-20 MuiChip-colorSecondary MuiChip-outlined MuiChip-outlinedSecondary"
class="MuiChip-root makeStyles-coveredMarker-23 MuiChip-colorPrimary MuiChip-outlined MuiChip-outlinedPrimary"
>
<span
class="MuiChip-label"
>
Targeted
Covered
</span>
</div>
<a
@ -549,7 +588,7 @@ exports[`renders the list of all rules 1`] = `
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-18 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
class="MuiButtonBase-root MuiChip-root makeStyles-coveredLanguageChip-17 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import { generate_rules_description } from '../description';
import { generateRulesDescription } from '../description';
import { withTestDir, createFiles } from '../testutils';
describe('description generation', () => {
@ -14,7 +14,7 @@ test('generates html from asciidoc', () => {
'Specific content'].join('\n')
});
return withTestDir(async (dstPath) => {
generate_rules_description(srcPath, dstPath);
generateRulesDescription(srcPath, dstPath);
const ruleHtml = fs.readFileSync(path.join(dstPath, 'S100', 'java-description.html'));
expect(ruleHtml.toString()).toEqual(

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import { generate_one_rule_metadata, generate_rules_metadata } from '../metadata';
import { generateOneRuleMetadata, generateRulesMetadata } from '../metadata';
import { withTestDir, createFiles } from '../testutils';
describe('metadata generation', () => {
@ -21,7 +21,7 @@ describe('metadata generation', () => {
}),
});
return withTestDir(async (dstPath) => {
generate_rules_metadata(srcPath, dstPath);
generateRulesMetadata(srcPath, dstPath);
const javaStrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const javaMetadata = JSON.parse(javaStrMetadata.toString());
expect(javaMetadata).toMatchObject({
@ -39,6 +39,39 @@ describe('metadata generation', () => {
});
});
test('check status computation', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/metadata.json': JSON.stringify({
title: 'Rule S100',
status: 'ready'
}),
'S100/java/metadata.json': JSON.stringify({
title: 'Java Rule S100'
}),
'S100/python/metadata.json': JSON.stringify({
status: 'closed'
}),
});
return withTestDir(async (dstPath) => {
generateRulesMetadata(srcPath, dstPath);
const javaStrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const pythonStrMetadata = fs.readFileSync(`${dstPath}/S100/python-metadata.json`);
const javaMetadata = JSON.parse(javaStrMetadata.toString());
const pythonMetadata = JSON.parse(pythonStrMetadata.toString());
expect(pythonMetadata).toMatchObject({
title: 'Rule S100',
languagesSupport: [
{name: 'java', status: 'ready'},
{name: 'python', status: 'closed'}
]
});
expect(javaMetadata.languagesSupport).toStrictEqual(pythonMetadata.languagesSupport);
});
});
});
test('generates only requested rules if a list of rule is provided', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
@ -50,7 +83,7 @@ describe('metadata generation', () => {
}),
});
return withTestDir(async (dstPath) => {
generate_rules_metadata(srcPath, dstPath, ['S100']);
generateRulesMetadata(srcPath, dstPath, ['S100']);
const s100Exists = fs.existsSync(`${dstPath}/S100/java-metadata.json`);
expect(s100Exists).toBeTruthy();
@ -72,7 +105,7 @@ describe('metadata generation', () => {
}),
});
return withTestDir(async (dstPath) => {
generate_one_rule_metadata(path.join(srcPath, 'S100'), path.join(dstPath, 'S100'), 'master');
generateOneRuleMetadata(path.join(srcPath, 'S100'), path.join(dstPath, 'S100'), 'master');
const s100StrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const s100Metadata = JSON.parse(s100StrMetadata.toString());
@ -80,7 +113,7 @@ describe('metadata generation', () => {
expect(s100Metadata.branch).toEqual('master');
expect(Object.keys(s100Metadata)).not.toContain('prUrl');
generate_one_rule_metadata(path.join(srcPath, 'S200'), path.join(dstPath, 'S200'), 'add-my-rule', 'https://some.pr/url');
generateOneRuleMetadata(path.join(srcPath, 'S200'), path.join(dstPath, 'S200'), 'add-my-rule', 'https://some.pr/url');
const s200StrMetadata = fs.readFileSync(`${dstPath}/S200/java-metadata.json`);
@ -91,4 +124,25 @@ describe('metadata generation', () => {
});
});
});
test('generate test metadata', () => {
return withTestDir(async (dstPath) => {
generateRulesMetadata(path.join(__dirname, 'resources', 'rules'), dstPath);
const rules = fs.readdirSync(dstPath);
expect(rules.length).toEqual(3);
let treated = 0;
rules.forEach(ruleDir => {
const languages = fs.readdirSync(`${dstPath}/${ruleDir}`);
expect(languages.length).toBeGreaterThanOrEqual(1);
languages.forEach(file => {
const actual = JSON.parse(fs.readFileSync(`${dstPath}/${ruleDir}/${file}`).toString());
const expectedPath = path.join(__dirname, 'resources', 'metadata', ruleDir, file);
const expected = JSON.parse(fs.readFileSync(expectedPath).toString());
expect(actual).toStrictEqual(expected);
treated++;
})
});
expect(treated).toBe(9);
});
});
});

View File

@ -2,6 +2,7 @@
"title": "Header files should not contain unnamed namespaces",
"type": "CODE_SMELL",
"status": "ready",
"quickfix": "unknown",
"remediation": {
"func": "Constant/Issue",
"constantCost": "1h"
@ -9,12 +10,12 @@
"tags": [
"cert",
"misra-c++2008",
"clumsy",
"pitfall"
],
"extra": {
"coveredLanguages": [
"C++",
"C"
"legacyKeys": [
"UnnamedNamespaceInHeader"
],
"replacementRules": []
},
@ -22,12 +23,20 @@
"ruleSpecification": "RSPEC-1000",
"sqKey": "UnnamedNamespaceInHeader",
"scope": "Main",
"securityStandards": {
"CERT": [
"DCL59-CPP."
]
},
"defaultQualityProfiles": [
"Sonar way",
"MISRA C++ 2008 recommended"
],
"all_languages": [
"cfamily"
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
}
],
"allKeys": [
"UnnamedNamespaceInHeader"

View File

@ -0,0 +1,85 @@
<div class="paragraph">
<p>An unnamed namespace will be unique within each translation unit. Any declarations appearing in an unnamed namespace in a header will refer to a different entity in each translation unit, which is probably not the expected behavior.</p>
</div>
<div class="sect1">
<h2 id="_noncompliant_code_example">Noncompliant Code Example</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre>// Header.hpp
namespace // Noncompliant
{
extern int32_t x;
}</pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre>// File1.cpp
#include "Header.hpp"
namespace
{
int32_t x;
}
void fn_a(void)
{
x = 42;
}</pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre>// File2.cpp
#include "Header.hpp"
namespace
{
int32_t x; // this is a different x than in File1.cpp
}
void fn_b(void)
{
fn_a(); // Is expected to initialize "x" to 42
if (x == 42) // But does not, as there are 2 distinct "x" variables
{
}
}</pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_see">See</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>MISRA C&#43;&#43;:2008, 7-3-3 - There shall be no unnamed namespaces in header files.</p>
</li>
<li>
<p><a href="https://wiki.sei.cmu.edu/confluence/x/VXs-BQ">CERT, DCL59-CPP.</a> - Do not define an unnamed namespace in a header file</p>
</li>
</ul>
</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_comments_and_links">Comments And Links</h2>
<div class="sectionbody">
<div class="paragraph">
<p>(visible only on this page)</p>
</div>
<div class="sect2">
<h3 id="_on_8_feb_2018_004304_thomas_epperson_wrote">on 8 Feb 2018, 00:43:04 Thomas Epperson wrote:</h3>
<div class="paragraph">
<p>The implementation incorrectly flags unnamed namespaces in source files. The specs only refer to unnamed namespaces in HEADER files.</p>
</div>
<div class="paragraph">
<p>See reported bugs in https://sonarcloud.io/dashboard?id=uglyoldbob_decompiler%3Arestructure</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,45 @@
{
"title": "Header files should not contain unnamed namespaces",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "1h"
},
"tags": [
"cert",
"misra-c++2008",
"clumsy",
"pitfall"
],
"extra": {
"replacementRules": [],
"legacyKeys": [
"UnnamedNamespaceInHeader"
]
},
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-1000",
"sqKey": "UnnamedNamespaceInHeader",
"scope": "Main",
"securityStandards": {
"CERT": [
"DCL59-CPP."
]
},
"defaultQualityProfiles": [
"Sonar way",
"MISRA C++ 2008 recommended"
],
"quickfix": "unknown",
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
}
],
"allKeys": [
"UnnamedNamespaceInHeader"
],
"branch": "master"
}

View File

@ -0,0 +1,54 @@
{
"title": "Printf-style format strings should be used correctly",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"tags": [
"cert",
"confusing",
"clumsy"
],
"defaultQualityProfiles": [
"Sonar way",
"MISRA C++ 2008 recommended"
],
"allKeys": [
"S3457"
],
"branch": "master",
"defaultSeverity": "Minor",
"extra": {
"legacyKeys": [],
"replacementRules": []
},
"quickfix": "unknown",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All",
"securityStandards": {
"CERT": [
"FIO47-C."
]
},
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
},
{
"name": "csharp",
"status": "ready"
},
{
"name": "java",
"status": "closed"
},
{
"name": "python",
"status": "deprecated"
}
]
}

View File

@ -0,0 +1,46 @@
{
"title": "Composite format strings should be used correctly",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"tags": [
"confusing"
],
"defaultQualityProfiles": [
"Sonar way"
],
"allKeys": [
"S3457"
],
"branch": "master",
"defaultSeverity": "Major",
"extra": {
"legacyKeys": [],
"replacementRules": []
},
"quickfix": "unknown",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All",
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
},
{
"name": "csharp",
"status": "ready"
},
{
"name": "java",
"status": "closed"
},
{
"name": "python",
"status": "deprecated"
}
]
}

View File

@ -0,0 +1,92 @@
<div class="paragraph">
<p>Because <code>printf</code> format strings are interpreted at runtime, rather than validated by the compiler, they can contain errors that result in the wrong strings being created. This rule statically validates the correlation of <code>printf</code> format strings to their arguments.</p>
</div>
<div class="paragraph">
<p>The related rule S2275 is about errors that will create undefined behavior, while this rule is about errors that produce an unexpected string.</p>
</div>
<div class="sect1">
<h2 id="_noncompliant_code_example">Noncompliant Code Example</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre>printf("%d", 1, 2); // Noncompliant; the second argument "2" is unused
printf("%0-f", 1.2); // Noncompliant; flag "0" is ignored because of "-"</pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_compliant_solution">Compliant Solution</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre>printf("%d %d", 1, 2); // Compliant
printf("%-f", 1.2); // Compliant</pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_exceptions">Exceptions</h2>
<div class="sectionbody">
<div class="paragraph">
<p>This rule will only work if the format string is provided as a string literal.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_see">See</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><a href="https://wiki.sei.cmu.edu/confluence/x/J9YxBQ">CERT, FIO47-C.</a> - Use valid format strings</p>
</li>
</ul>
</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_implementation_specification">Implementation Specification</h2>
<div class="sectionbody">
<div class="paragraph">
<p>(visible only on this page)</p>
</div>
<div class="sect2">
<h3 id="_message">Message</h3>
<div class="paragraph">
<p>XXXX</p>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_comments_and_links">Comments And Links</h2>
<div class="sectionbody">
<div class="paragraph">
<p>(visible only on this page)</p>
</div>
<div class="sect2">
<h3 id="_is_duplicated_by_s3941">is duplicated by: S3941</h3>
</div>
<div class="sect2">
<h3 id="_is_related_to_s2275">is related to: S2275</h3>
</div>
<div class="sect2">
<h3 id="_on_10_dec_2015_090759_tamas_vajk_wrote">on 10 Dec 2015, 09:07:59 Tamas Vajk wrote:</h3>
<div class="paragraph">
<p>\[~ann.campbell.2] Removed the performance label, as the performance impact is insignificant.</p>
</div>
</div>
<div class="sect2">
<h3 id="_on_10_dec_2015_144405_ann_campbell_wrote">on 10 Dec 2015, 14:44:05 Ann Campbell wrote:</h3>
<div class="paragraph">
<p>I&#8217;ve updated SQALE characteristic to match [~tamas.vajk]</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,54 @@
{
"title": "Printf-style format strings should be used correctly",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "10min"
},
"tags": [
"cert",
"confusing",
"clumsy"
],
"extra": {
"replacementRules": [],
"legacyKeys": []
},
"defaultSeverity": "Minor",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All",
"defaultQualityProfiles": [
"Sonar way",
"MISRA C++ 2008 recommended"
],
"quickfix": "unknown",
"securityStandards": {
"CERT": [
"FIO47-C."
]
},
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
},
{
"name": "csharp",
"status": "ready"
},
{
"name": "java",
"status": "closed"
},
{
"name": "python",
"status": "deprecated"
}
],
"allKeys": [
"S3457"
],
"branch": "master"
}

View File

@ -0,0 +1,51 @@
{
"title": "Composite format strings should be used correctly",
"type": "CODE_SMELL",
"status": "closed",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"allKeys": [
"S3457"
],
"tags": [
"cert",
"confusing"
],
"branch": "master",
"defaultQualityProfiles": [
"Sonar way"
],
"defaultSeverity": "Major",
"extra": {
"legacyKeys": [],
"replacementRules": []
},
"quickfix": "unknown",
"securityStandards": {
"CERT": [
"FIO47-C."
]
}, "ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All",
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
},
{
"name": "csharp",
"status": "ready"
},
{
"name": "java",
"status": "closed"
},
{
"name": "python",
"status": "deprecated"
}
]
}

View File

@ -0,0 +1,46 @@
{
"title": "String formatting should be used correctly",
"type": "CODE_SMELL",
"status": "deprecated",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"tags": [
"confusing"
],
"allKeys": [
"S3457"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All",
"branch": "master",
"defaultQualityProfiles": [
"Sonar way"
],
"extra": {
"legacyKeys": [],
"replacementRules": []
},
"quickfix": "unknown",
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
},
{
"name": "csharp",
"status": "ready"
},
{
"name": "java",
"status": "closed"
},
{
"name": "python",
"status": "deprecated"
}
]
}

View File

@ -0,0 +1,35 @@
{
"title": "\"<signal.h>\" should not be used",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "4h"
},
"allKeys": [
"PPIncludeSignal"
],
"tags": [
"based-on-misra",
"lock-in"
],
"defaultSeverity": "Critical",
"branch": "master",
"defaultQualityProfiles": [],
"extra": {
"legacyKeys": [
"PPIncludeSignal"
],
"replacementRules": []
},
"quickfix": "unknown",
"ruleSpecification": "RSPEC-987",
"sqKey": "PPIncludeSignal",
"scope": "Main",
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
}
]
}

View File

@ -0,0 +1,47 @@
<div class="paragraph">
<p>Signal handling contains implementation-defined and undefined behavior.</p>
</div>
<div class="sect1">
<h2 id="_noncompliant_code_example">Noncompliant Code Example</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre>#include &lt;signal.h&gt; /* Noncompliant */</pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_see">See</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>MISRA C:2004, 20.8 - The signal handling facilities of &lt;signal.h&gt; shall not be used.</p>
</li>
<li>
<p>MISRA C:2012, 21.5 - The standard header file &lt;signal.h&gt; shall not be used</p>
</li>
</ul>
</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_comments_and_links">Comments And Links</h2>
<div class="sectionbody">
<div class="paragraph">
<p>(visible only on this page)</p>
</div>
<div class="sect2">
<h3 id="_is_related_to_s1054">is related to: S1054</h3>
</div>
<div class="sect2">
<h3 id="_on_31_mar_2015_190723_evgeny_mandrikov_wrote">on 31 Mar 2015, 19:07:23 Evgeny Mandrikov wrote:</h3>
<div class="paragraph">
<p>\[~ann.campbell.2] implementation seems more complete (SQALE, description) than this spec.</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,35 @@
{
"title": "\"<signal.h>\" should not be used",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "4h"
},
"tags": [
"based-on-misra",
"lock-in"
],
"extra": {
"replacementRules": [],
"legacyKeys": [
"PPIncludeSignal"
]
},
"defaultSeverity": "Critical",
"ruleSpecification": "RSPEC-987",
"sqKey": "PPIncludeSignal",
"scope": "Main",
"defaultQualityProfiles": [],
"quickfix": "unknown",
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
}
],
"allKeys": [
"PPIncludeSignal"
],
"branch": "master"
}

View File

@ -1,17 +0,0 @@
{
"title": "Printf-style format strings should be used correctly",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"tags": [
"cert",
"confusing"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All"
}

View File

@ -1,16 +0,0 @@
{
"title": "String formatting should be used correctly",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"tags": [
"confusing"
],
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All"
}

View File

@ -0,0 +1,8 @@
=== on 8 Feb 2018, 00:43:04 Thomas Epperson wrote:
The implementation incorrectly flags unnamed namespaces in source files. The specs only refer to unnamed namespaces in HEADER files.
See reported bugs in \https://sonarcloud.io/dashboard?id=uglyoldbob_decompiler%3Arestructure

View File

@ -0,0 +1,37 @@
{
"title": "Header files should not contain unnamed namespaces",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "1h"
},
"tags": [
"cert",
"misra-c++2008",
"clumsy",
"pitfall"
],
"extra": {
"replacementRules": [
],
"legacyKeys": [
"UnnamedNamespaceInHeader"
]
},
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-1000",
"sqKey": "UnnamedNamespaceInHeader",
"scope": "Main",
"securityStandards": {
"CERT": [
"DCL59-CPP."
]
},
"defaultQualityProfiles": [
"Sonar way",
"MISRA C++ 2008 recommended"
],
"quickfix": "unknown"
}

View File

@ -0,0 +1,60 @@
An unnamed namespace will be unique within each translation unit. Any declarations appearing in an unnamed namespace in a header will refer to a different entity in each translation unit, which is probably not the expected behavior.
== Noncompliant Code Example
----
// Header.hpp
namespace // Noncompliant
{
extern int32_t x;
}
----
----
// File1.cpp
#include "Header.hpp"
namespace
{
int32_t x;
}
void fn_a(void)
{
x = 42;
}
----
----
// File2.cpp
#include "Header.hpp"
namespace
{
int32_t x; // this is a different x than in File1.cpp
}
void fn_b(void)
{
fn_a(); // Is expected to initialize "x" to 42
if (x == 42) // But does not, as there are 2 distinct "x" variables
{
}
}
----
== See
* MISRA {cpp}:2008, 7-3-3 - There shall be no unnamed namespaces in header files.
* https://wiki.sei.cmu.edu/confluence/x/VXs-BQ[CERT, DCL59-CPP.] - Do not define an unnamed namespace in a header file
ifdef::env-github,rspecator-view[]
'''
== Comments And Links
(visible only on this page)
include::comments-and-links.adoc[]
endif::env-github,rspecator-view[]

View File

@ -0,0 +1,2 @@
{
}

View File

@ -1,21 +1,18 @@
{
"title": "Printf-style format strings should be used correctly",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "10min"
},
"tags": [
"cert",
"confusing",
"clumsy"
],
"defaultQualityProfiles": [
"Sonar way",
"MISRA C++ 2008 recommended"
],
"defaultSeverity": "Minor",
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All"
"securityStandards": {
"CERT": [
"FIO47-C."
]
}
}

View File

@ -0,0 +1,41 @@
Because ``++printf++`` format strings are interpreted at runtime, rather than validated by the compiler, they can contain errors that result in the wrong strings being created. This rule statically validates the correlation of ``++printf++`` format strings to their arguments.
The related rule S2275 is about errors that will create undefined behavior, while this rule is about errors that produce an unexpected string.
== Noncompliant Code Example
----
printf("%d", 1, 2); // Noncompliant; the second argument "2" is unused
printf("%0-f", 1.2); // Noncompliant; flag "0" is ignored because of "-"
----
== Compliant Solution
----
printf("%d %d", 1, 2); // Compliant
printf("%-f", 1.2); // Compliant
----
== Exceptions
This rule will only work if the format string is provided as a string literal.
== See
* https://wiki.sei.cmu.edu/confluence/x/J9YxBQ[CERT, FIO47-C.] - Use valid format strings
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
include::../message.adoc[]
'''
== Comments And Links
(visible only on this page)
include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]

View File

@ -0,0 +1,10 @@
=== is duplicated by: S3941
=== is related to: S2275
=== on 10 Dec 2015, 09:07:59 Tamas Vajk wrote:
\[~ann.campbell.2] Removed the performance label, as the performance impact is insignificant.
=== on 10 Dec 2015, 14:44:05 Ann Campbell wrote:
I've updated SQALE characteristic to match [~tamas.vajk]

View File

@ -0,0 +1,29 @@
== Compliant Solution
----
String.format("First %s and then %s", "foo", "bar");
String.format("Display %2$d and then %d", 1, 3);
String.format("Too many arguments %d %d", 1, 2);
String.format("First Line%n");
String.format("Is myObject null ? %b", myObject == null);
String.format("value is %d", value);
String s = "string without arguments";
MessageFormat.format("Result {0}.", value);
MessageFormat.format("Result '{0}' = {0}", value);
MessageFormat.format("Result {0}.", myObject);
java.util.Logger logger;
logger.log(java.util.logging.Level.SEVERE, "Result {0}.", myObject);
logger.log(java.util.logging.Level.SEVERE, "Result {0}'", 14);
logger.log(java.util.logging.Level.SEVERE, exception, () -> "Result " + param);
org.slf4j.Logger slf4jLog;
org.slf4j.Marker marker;
slf4jLog.debug(marker, "message {}");
slf4jLog.debug(marker, "message {}", 1);
org.apache.logging.log4j.Logger log4jLog;
log4jLog.debug("message {}", 1);
----

View File

@ -0,0 +1,3 @@
{
"title": "Composite format strings should be used correctly"
}

View File

@ -0,0 +1,51 @@
Because composite format strings are interpreted at runtime, rather than validated by the compiler, they can contain errors that lead to unexpected behaviors or runtime errors. This rule statically validates the good behavior of composite formats when calling the methods of ``++String.Format++``, ``++StringBuilder.AppendFormat++``, ``++Console.Write++``, ``++Console.WriteLine++``, ``++TextWriter.Write++``, ``++TextWriter.WriteLine++``, ``++Debug.WriteLine(String,Object[])++``, ``++Trace.TraceError(String,Object[])++``, ``++Trace.TraceInformation(String,Object[])++``, ``++Trace.TraceWarning(String,Object[])++`` and ``++TraceSource.TraceInformation(String,Object[])++``.
== Noncompliant Code Example
----
s = string.Format("{0}", arg0, arg1); // Noncompliant, arg1 is declared but not used.
s = string.Format("{0} {2}", arg0, arg1, arg2); // Noncompliant, the format item with index 1 is missing so arg1 will not be used.
s = string.Format("foo"); // Noncompliant, there is no need to use string.Format here.
----
== Compliant Solution
----
s = string.Format("{0}", arg0);
s = string.Format("{0} {1}", arg0, arg2);
s = "foo";
----
== Exceptions
* No issue is raised if the format string is not a ``++const++``.
----
var pattern = "{0} {1} {2}";
var res = string.Format(pattern, 1, 2); // Compliant, not const string are not recognized
----
* No issue is raised if the argument is not an inline creation array.
----
var array = new int[] {};
var res = string.Format("{0} {1}", array); // Compliant we don't know the size of the array
----
* This rule doesn't check whether the format specifier (defined after the ``++:++``) is actually valid.
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
include::../message.adoc[]
'''
== Comments And Links
(visible only on this page)
include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]

View File

@ -0,0 +1 @@
Because ``++printf++``-style format strings are interpreted at runtime, rather than validated by the compiler, they can contain errors that result in the wrong strings being created. This rule statically validates the correlation of ``++printf++``-style format strings to their arguments when calling the ``++format(...)++`` methods of ``++java.util.Formatter++``, ``++java.lang.String++``, ``++java.io.PrintStream++``, ``++MessageFormat++``, and ``++java.io.PrintWriter++`` classes and the ``++printf(...)++`` methods of ``++java.io.PrintStream++`` or ``++java.io.PrintWriter++`` classes.

View File

@ -0,0 +1,12 @@
{
"tags": [
"cert",
"confusing"
],
"status": "closed",
"securityStandards": {
"CERT": [
"FIO47-C."
]
}
}

View File

@ -0,0 +1,24 @@
include::../description.adoc[]
include::../noncompliant.adoc[]
include::../compliant.adoc[]
== See
* https://wiki.sei.cmu.edu/confluence/x/J9YxBQ[CERT, FIO47-C.] - Use valid format strings
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
include::../message.adoc[]
'''
== Comments And Links
(visible only on this page)
include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]

View File

@ -0,0 +1,4 @@
=== Message
XXXX

View File

@ -16,13 +16,12 @@
"ruleSpecification": "RSPEC-3457",
"sqKey": "S3457",
"scope": "All",
"all_languages": [
"cfamily",
"csharp",
"java",
"python"
],
"allKeys": [
"S3457"
]
],
"extra": {
"replacementRules": [],
"legacyKeys": []
},
"quickfix": "unknown"
}

View File

@ -0,0 +1,30 @@
== Noncompliant Code Example
----
String.format("First {0} and then {1}", "foo", "bar"); //Noncompliant. Looks like there is a confusion with the use of {{java.text.MessageFormat}}, parameters "foo" and "bar" will be simply ignored here
String.format("Display %3$d and then %d", 1, 2, 3); //Noncompliant; the second argument '2' is unused
String.format("Too many arguments %d and %d", 1, 2, 3); //Noncompliant; the third argument '3' is unused
String.format("First Line\n"); //Noncompliant; %n should be used in place of \n to produce the platform-specific line separator
String.format("Is myObject null ? %b", myObject); //Noncompliant; when a non-boolean argument is formatted with %b, it prints true for any nonnull value, and false for null. Even if intended, this is misleading. It's better to directly inject the boolean value (myObject == null in this case)
String.format("value is " + value); // Noncompliant
String s = String.format("string without arguments"); // Noncompliant
MessageFormat.format("Result '{0}'.", value); // Noncompliant; String contains no format specifiers. (quote are discarding format specifiers)
MessageFormat.format("Result {0}.", value, value); // Noncompliant; 2nd argument is not used
MessageFormat.format("Result {0}.", myObject.toString()); // Noncompliant; no need to call toString() on objects
java.util.Logger logger;
logger.log(java.util.logging.Level.SEVERE, "Result {0}.", myObject.toString()); // Noncompliant; no need to call toString() on objects
logger.log(java.util.logging.Level.SEVERE, "Result.", new Exception()); // compliant, parameter is an exception
logger.log(java.util.logging.Level.SEVERE, "Result '{0}'", 14); // Noncompliant - String contains no format specifiers.
logger.log(java.util.logging.Level.SEVERE, "Result " + param, exception); // Noncompliant; Lambda should be used to differ string concatenation.
org.slf4j.Logger slf4jLog;
org.slf4j.Marker marker;
slf4jLog.debug(marker, "message {}");
slf4jLog.debug(marker, "message", 1); // Noncompliant - String contains no format specifiers.
org.apache.logging.log4j.Logger log4jLog;
log4jLog.debug("message", 1); // Noncompliant - String contains no format specifiers.
----

View File

@ -0,0 +1,19 @@
=== Message
* Add replacement fields or use a normal string instead of an f-string.
* Remove this unused argument.
* Remove X unused positional arguments.
* Fix this formatted string's syntax.
* Name unnamed replacement field(s).
* Replace formatting argument(s) with a mapping; Replacement fields are named.
* Use only positional or named fields, don't mix them.
* Replace this value with a number as "%d" requires.
* Replace this value with an integer as "%X" requires.
* Replace this value with an integer as "*" requires.
* Add X missing argument(s).
* Remove X unexpected argument(s); format string expects Y arguments.
* Replace this key; %-format accepts only string keys.
* Provide a value for field "X".
* Remove this unused argument or add a replacement field.

View File

@ -0,0 +1,4 @@
{
"title": "String formatting should be used correctly",
"status": "deprecated"
}

View File

@ -0,0 +1,70 @@
Formatting strings, either with the ``++%++`` operator or ``++str.format++`` method, requires a valid string and arguments matching this string's replacement fields.
This also applies to loggers from the ``++logging++`` module. Internally they use ``++%-formatting++``. The only difference is that they will log an error instead of raising an exception when provided arguments are invalid.
Formatted string literals, also called "f-strings", are generally simpler to use, and any syntax mistake will fail at compile time. However it is easy to forget curly braces and it won't raise any error.
This rule raises an issue when:
* A string formatted with ``++%++`` will not return the expected string because some arguments are not used.
* A string formatted with ``++str.format++`` will not return the expected string because some arguments are not used.
* An "f-string" doesn't contain any replacement field, which probably means that some curly braces are missing.
* Loggers will log an error because their message is not formatted properly.
Rule S2275 covers cases where formatting a string will raise an exception.
== Noncompliant Code Example
----
"Error %(message)s" % {"message": "something failed", "extra": "some dead code"} # Noncompliant. Remove the unused argument "extra" or add a replacement field.
"Error: User {} has not been able to access []".format("Alice", "MyFile") # Noncompliant. Remove 1 unexpected argument or add a replacement field.
user = "Alice"
resource = "MyFile"
message = f"Error: User [user] has not been able to access [resource]" # Noncompliant. Add replacement fields or use a normal string instead of an f-string.
import logging
logging.error("Error: User %s has not been able to access %s", "Alice") # Noncompliant. Add 1 missing argument.
----
== Compliant Solution
----
"Error %(message)s" % {"message": "something failed"}
"Error: User {} has not been able to access {}".format("Alice", "MyFile")
user = "Alice"
resource = "MyFile"
message = f"Error: User {user} has not been able to access {resource}"
import logging
logging.error("Error: User %s has not been able to access %s", "Alice", "MyFile")
----
== See
* https://docs.python.org/3/library/string.html#format-string-syntax[Python documentation - Format String Syntax]
* https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting[Python documentation - printf-style String Formatting]
* https://docs.python.org/3/howto/logging.html#loggers[Python documentation - Loggers]
* https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application[Python documentation - Using particular formatting styles throughout your application]
* https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals[Python documentation - Formatted string literals]
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
include::message.adoc[]
'''
== Comments And Links
(visible only on this page)
include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]

View File

@ -0,0 +1,6 @@
include::description.adoc[]
include::noncompliant.adoc[]
include::compliant.adoc[]

View File

@ -0,0 +1,5 @@
=== is related to: S1054
=== on 31 Mar 2015, 19:07:23 Evgeny Mandrikov wrote:
\[~ann.campbell.2] implementation seems more complete (SQALE, description) than this spec.

View File

@ -10,8 +10,20 @@
"based-on-misra",
"lock-in"
],
"extra": {
"replacementRules": [
],
"legacyKeys": [
"PPIncludeSignal"
]
},
"defaultSeverity": "Critical",
"ruleSpecification": "RSPEC-987",
"sqKey": "PPIncludeSignal",
"scope": "Main"
"scope": "Main",
"defaultQualityProfiles": [
],
"quickfix": "unknown"
}

View File

@ -0,0 +1,23 @@
Signal handling contains implementation-defined and undefined behavior.
== Noncompliant Code Example
----
#include <signal.h> /* Noncompliant */
----
== See
* MISRA C:2004, 20.8 - The signal handling facilities of <signal.h> shall not be used.
* MISRA C:2012, 21.5 - The standard header file <signal.h> shall not be used
ifdef::env-github,rspecator-view[]
'''
== Comments And Links
(visible only on this page)
include::comments-and-links.adoc[]
endif::env-github,rspecator-view[]

View File

@ -0,0 +1,2 @@
{
}

View File

@ -6,14 +6,20 @@ import { buildSearchIndex, buildIndexStore, DESCRIPTION_SPLIT_REGEX } from '../s
describe('index store generation', () => {
test('merges rules metadata', () => {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
const ruleS3457 = indexStore['S3457'];
expect(ruleS3457).toMatchObject({
id: 'S3457',
type: 'CODE_SMELL',
languages: ['cfamily', 'csharp', 'java', 'python'],
languages: [
{ "name": "cfamily", "status": "ready", },
{ "name": "csharp", "status": "ready", },
{ "name": "default", "status": "ready", },
{ "name": "java", "status": "closed", },
{ "name": "python", "status": "deprecated", }
],
tags: ['cert', 'clumsy', 'confusing'],
severities: ['Major', 'Minor'],
qualityProfiles: ['MISRA C++ 2008 recommended', 'Sonar way'],
@ -21,7 +27,7 @@ describe('index store generation', () => {
});
test('stores description words', () => {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
const ruleS3457 = indexStore['S3457'];
@ -34,28 +40,30 @@ describe('index store generation', () => {
});
test('collects all tags', () => {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [_, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.tags).toEqual({"based-on-misra": 1,
"cert": 5,
"clumsy": 4,
"confusing": 4,
"cert": 2,
"clumsy": 2,
"confusing": 1,
"lock-in": 1,
"misra-c++2008": 1,
"pitfall": 1});
"pitfall": 1
});
});
test('collects all languages', () => {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [_, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.langs).toEqual({"cfamily": 3,
"csharp": 1,
"default": 3,
"java": 1,
"python": 1});
});
test('collects all rule keys', () => {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
expect(indexStore['S3457'].all_keys).toEqual(['RSPEC-3457', 'S3457']);
expect(indexStore['S1000'].all_keys).toEqual(['RSPEC-1000', 'S1000', 'UnnamedNamespaceInHeader']);
@ -63,7 +71,7 @@ describe('index store generation', () => {
});
test('collects all quality profiles', () => {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [_, aggregates] = buildIndexStore(rulesPath);
expect(aggregates.qualityProfiles).toEqual({
"MISRA C++ 2008 recommended": 2,
@ -72,7 +80,7 @@ describe('index store generation', () => {
});
function createIndex() {
const rulesPath = path.join(__dirname, 'resources', 'plugin_rules');
const rulesPath = path.join(__dirname, 'resources', 'metadata');
const [indexStore, _] = buildIndexStore(rulesPath);
// Hack to avoid warnings when 'selectivePipeline' is already registered
@ -141,7 +149,9 @@ describe('search index enables search by tags and quality profiles', () => {
test('searches in rule tags', () => {
const searchIndex = createIndex();
const searchesS3457 = search(searchIndex, 'cert', 'tags');
expect(searchesS3457).toEqual(['S1000', 'S3457']);
expect(searchesS3457).toHaveLength(2);
expect(searchesS3457).toContain('S1000');
expect(searchesS3457).toContain('S3457');
const searchesS987 = search(searchIndex, 'based-on-misra', 'tags');
expect(searchesS987).toEqual(['S987']);

View File

@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path';
import asciidoctor from 'asciidoctor';
import { getRulesDirectories, listSupportedLanguage } from './utils';
import { getRulesDirectories, listSupportedLanguages } from './utils';
import { logger } from './deploymentLogger';
const asciidoc = asciidoctor();
@ -30,17 +30,17 @@ asciidoc.LoggerManager.setLogger(winstonLogger);
* @param srcDir directory containing the original rule metadata and description.
* @param dstDir directory where the generated rules metadata and description will be written.
*/
export function generate_one_rule_description(srcDir: string, dstDir: string) {
export function generateOneRuleDescription(srcDir: string, dstDir: string) {
fs.mkdirSync(dstDir, { recursive: true });
const all_languages = listSupportedLanguage(srcDir);
const languages = listSupportedLanguages(srcDir);
let default_descr_wanted = true;
for (const language of all_languages) {
const html = generate_rule_description(srcDir, language);
const dstFile = path.join(dstDir, language + "-description.html");
for (const language of languages) {
const html = generateRuleDescription(srcDir, language);
const dstFile = path.join(dstDir, language + '-description.html');
fs.writeFileSync(dstFile, html, {encoding: 'utf8'});
if (default_descr_wanted) {
const dstFile = path.join(dstDir, "default-description.html");
fs.writeFileSync(dstFile, html, {encoding: 'utf8'});
const defFile = path.join(dstDir, 'default-description.html');
fs.writeFileSync(defFile, html, {encoding: 'utf8'});
default_descr_wanted = false;
}
}
@ -52,9 +52,9 @@ export function generate_one_rule_description(srcDir: string, dstDir: string) {
* @param dstPath directory where the generated rules metadata and description will be written.
* @param rules an optional list of rules to list. Other rules won't be generated.
*/
export function generate_rules_description(srcPath: string, dstPath: string, rules?: string[]) {
export function generateRulesDescription(srcPath: string, dstPath: string, rules?: string[]) {
for (const { srcDir, dstDir } of getRulesDirectories(srcPath, dstPath, rules)) {
generate_one_rule_description(srcDir, dstDir);
generateOneRuleDescription(srcDir, dstDir);
}
}
@ -63,12 +63,12 @@ export function generate_rules_description(srcPath: string, dstPath: string, rul
* @param srcDir rule's source directory.
* @param language language for which the metadata should be generated
*/
function generate_rule_description(srcDir: string, language: string) {
let ruleSrcFile = path.join(srcDir, language, "rule.adoc");
function generateRuleDescription(srcDir: string, language: string) {
let ruleSrcFile = path.join(srcDir, language, 'rule.adoc');
if (!fs.existsSync(ruleSrcFile)) {
ruleSrcFile = path.join(srcDir, "rule.adoc");
ruleSrcFile = path.join(srcDir, 'rule.adoc');
if (!fs.existsSync(ruleSrcFile)) {
throw new Error("Missing file 'rule.adoc' for language '" + language + " in " + srcDir);
throw new Error(`Missing file 'rule.adoc' for language ${language} in ${srcDir}`);
}
}
const baseDir = path.resolve(path.dirname(ruleSrcFile));

View File

@ -5,8 +5,8 @@ import yargs from 'yargs/yargs';
import path from 'path';
import { clean_rules } from './clean';
import { generate_one_rule_metadata, generate_rules_metadata } from './metadata';
import { generate_one_rule_description, generate_rules_description, } from './description';
import { generateOneRuleMetadata, generateRulesMetadata } from './metadata';
import { generateOneRuleDescription, generateRulesDescription, } from './description';
import { createIndexFiles } from './searchIndex';
import { process_incomplete_rspecs, PullRequest } from './pullRequestIndexing';
@ -24,10 +24,10 @@ yargs(process.argv.slice(2))
yargs.array<string>('rules')
},
(argv: any) => {
generate_rules_metadata(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY, argv.rules)
generateRulesMetadata(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY, argv.rules);
process_incomplete_rspecs(PR_DIRECTORY, function (srcDir: string, pr: PullRequest) {
const dstDir = path.join(RULE_DST_DIRECTORY, pr.rspec_id);
generate_one_rule_metadata(srcDir, dstDir, pr.branch, pr.url);
generateOneRuleMetadata(srcDir, dstDir, pr.branch, pr.url);
})
})
@ -36,10 +36,10 @@ yargs(process.argv.slice(2))
yargs.array<string>('rules')
},
(argv: any) => {
generate_rules_description(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY, argv.rules)
generateRulesDescription(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY, argv.rules);
process_incomplete_rspecs(PR_DIRECTORY, function (srcDir: string, pr: PullRequest) {
const dstDir = path.join(RULE_DST_DIRECTORY, pr.rspec_id);
generate_one_rule_description(srcDir, dstDir);
generateOneRuleDescription(srcDir, dstDir);
})
})
@ -50,12 +50,12 @@ yargs(process.argv.slice(2))
.command('*', 'generate rules metadata, description and index',
() => {},
(argv: any) => {
generate_rules_metadata(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY);
generate_rules_description(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY);
generateRulesMetadata(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY);
generateRulesDescription(RULE_SRC_DIRECTORY, RULE_DST_DIRECTORY);
process_incomplete_rspecs(PR_DIRECTORY, function (srcDir: string, pr: PullRequest) {
const dstDir = path.join(RULE_DST_DIRECTORY, pr.rspec_id);
generate_one_rule_metadata(srcDir, dstDir, pr.branch, pr.url);
generate_one_rule_description(srcDir, dstDir);
generateOneRuleMetadata(srcDir, dstDir, pr.branch, pr.url);
generateOneRuleDescription(srcDir, dstDir);
}).then(function() {
createIndexFiles(RULE_DST_DIRECTORY);
});

View File

@ -1,7 +1,8 @@
import fs from 'fs';
import path from 'path';
import { LanguageSupport } from '../types/RuleMetadata';
import { getRulesDirectories, listSupportedLanguage } from './utils';
import { getRulesDirectories, listSupportedLanguages } from './utils';
/**
* Generate rule metadata (for all relevant languages) and write it in the destination directory.
@ -10,15 +11,19 @@ import { getRulesDirectories, listSupportedLanguage } from './utils';
* @param branch the branch containing the given version of the rule. Typically 'master' but can be different for not merged rules.
* @param prUrl optional link to the PR adding the rule. absent for merged rules.
*/
export function generate_one_rule_metadata(srcDir: string, dstDir: string,
export function generateOneRuleMetadata(srcDir: string, dstDir: string,
branch: string, prUrl?: string) {
fs.mkdirSync(dstDir, { recursive: true });
const allLanguages = listSupportedLanguage(srcDir);
const allLanguages = listSupportedLanguages(srcDir);
const allMetadata = allLanguages.map((language) => {
const metadata = generate_rule_metadata(srcDir, language, allLanguages);
const metadata = generateRuleMetadata(srcDir, language);
return {language, metadata};
});
// Update language status for all
const languageSupports =
allMetadata.map(m => ({name: m.language, status: m.metadata.status} as LanguageSupport));
// Merge all sqKeys in an array so that we can use it later to check rule coverage.
const allKeys = allMetadata
.reduce((set, {metadata}) => {
@ -33,14 +38,15 @@ export function generate_one_rule_metadata(srcDir: string, dstDir: string,
metadata.prUrl = prUrl;
}
metadata.branch = branch;
metadata.languagesSupport = languageSupports;
});
let default_metadata_wanted = true;
for (const { language, metadata } of allMetadata) {
const dstJsonFile = path.join(dstDir, language + "-metadata.json");
const dstJsonFile = path.join(dstDir, language + '-metadata.json');
fs.writeFileSync(dstJsonFile, JSON.stringify(metadata, null, 2), { encoding: 'utf8' })
if (default_metadata_wanted) {
const dstFile = path.join(dstDir, "default-metadata.json");
const dstFile = path.join(dstDir, 'default-metadata.json');
fs.writeFileSync(dstFile, JSON.stringify(metadata, null, 2), { encoding: 'utf8' });
default_metadata_wanted = false;
}
@ -53,9 +59,9 @@ export function generate_one_rule_metadata(srcDir: string, dstDir: string,
* @param dstPath directory where the generated rules metadata and description will be written.
* @param rules an optional list of rules to list. Other rules won't be generated.
*/
export function generate_rules_metadata(srcPath: string, dstPath: string, rules?: string[]) {
export function generateRulesMetadata(srcPath: string, dstPath: string, rules?: string[]) {
for (const { srcDir, dstDir } of getRulesDirectories(srcPath, dstPath, rules)) {
generate_one_rule_metadata(srcDir, dstDir, 'master');
generateOneRuleMetadata(srcDir, dstDir, 'master');
}
}
@ -63,14 +69,11 @@ export function generate_rules_metadata(srcPath: string, dstPath: string, rules?
* Generate the metadata corresponding to one rule and one language.
* @param srcDir rule's source directory.
* @param language language for which the metadata should be generated
* @param all_languages every language the rule supports.
*/
function generate_rule_metadata(srcDir: string, language: string, all_languages: string[]) {
let parentFile = path.join(srcDir, language, "metadata.json");
function generateRuleMetadata(srcDir: string, language: string) {
const parentFile = path.join(srcDir, language, 'metadata.json');
const parentJson = fs.existsSync(parentFile) ? JSON.parse(fs.readFileSync(parentFile, 'utf8')) : {};
let childFile = path.join(srcDir, "metadata.json");
const childFile = path.join(srcDir, 'metadata.json');
const childJson = fs.existsSync(childFile) ? JSON.parse(fs.readFileSync(childFile, 'utf8')) : {};
const mergedJson = {...childJson, ...parentJson};
mergedJson["all_languages"] = all_languages;
return mergedJson;
return {...childJson, ...parentJson};
}

View File

@ -7,6 +7,7 @@ import lunr, { Token } from 'lunr';
import { IndexedRule, IndexStore, Severity, IndexAggregates } from '../types/IndexStore';
import { logger as rootLogger } from './deploymentLogger';
import { LanguageSupport } from '../types/RuleMetadata';
const logger = rootLogger.child({ source: path.basename(__filename) })
@ -17,33 +18,16 @@ export interface IndexedRuleWithDescription extends IndexedRule {
descriptions?: Array<string>;
}
/**
* Create the index store. This store is indexed by lunr and later used by the frontend.
* Whenever the lunr index finds something it returns IDs. The frontend will look at
* metadata corresponding to this ID in the index store.
* @param rulesPath Path to the directory containing aggregated metadata and rules
* descriptions in HTML format.
*/
export function buildIndexStore(rulesPath: string):[Record<string,IndexedRuleWithDescription>, IndexAggregates] {
let ruleDirs = fs.readdirSync(rulesPath).filter((fileName) => {
const fullpath = path.join(rulesPath, fileName);
return fs.lstatSync(fullpath).isDirectory();
});
let allTags: { [id: string]: number } = {};
let allLangs: { [id: string]: number } = {};
let allQualityProfiles: { [id: string]: number } = {};
const indexedRecords = ruleDirs.map<[string, IndexedRuleWithDescription] | null>((ruleDir) => {
const allLanguages = fs.readdirSync(path.join(rulesPath, ruleDir))
.filter((fileName) => fileName.endsWith('-metadata.json'))
.map((fileName) => fileName.split('-')[0]);
function buildOneRuleRecord(allLanguages: string[], rulesPath: string, ruleDir: string) {
let types = new Set<string>();
let severities = new Set<Severity>();
const all_keys = new Set<string>([ruleDir]);
const allKeys = new Set<string>([ruleDir]);
const titles = new Set<string>();
const tags = new Set<string>();
const qualityProfiles = new Set<string>();
const descriptions = new Set<string>();
const supportedLanguages : LanguageSupport[] = [];
let prUrl = undefined;
allLanguages.forEach((lang) => {
@ -63,11 +47,12 @@ export function buildIndexStore(rulesPath: string):[Record<string,IndexedRuleWit
if (metadata.prUrl) {
prUrl = metadata.prUrl;
}
all_keys.add(metadata.sqKey);
all_keys.add(metadata.ruleSpecification);
allKeys.add(metadata.sqKey);
allKeys.add(metadata.ruleSpecification);
titles.add(metadata.title);
types.add(metadata.type);
severities.add(metadata.defaultSeverity as Severity);
supportedLanguages.push({name: lang, status: metadata.status});
if (metadata.tags) {
for (const tag of metadata.tags) {
tags.add(tag);
@ -78,60 +63,103 @@ export function buildIndexStore(rulesPath: string):[Record<string,IndexedRuleWit
qualityProfiles.add(qualityProfile);
}
}
if (lang in allLangs) {
allLangs[lang] += 1;
} else {
allLangs[lang] = 1;
}
tags.forEach((tag) => {
if (tag in allTags) {
allTags[tag] += 1;
} else {
allTags[tag] = 1;
}
});
});
qualityProfiles.forEach((qualityProfile) => {
if (qualityProfile in allQualityProfiles) {
allQualityProfiles[qualityProfile] += 1;
} else {
allQualityProfiles[qualityProfile] = 1;
return {
types,
severities,
allKeys,
titles,
supportedLanguages,
tags,
qualityProfiles,
descriptions,
prUrl
};
}
});
function buildOneRuleIndexedRecord(rulesPath: string, ruleDir: string)
: [string, IndexedRuleWithDescription] | null {
const allLanguages = fs.readdirSync(path.join(rulesPath, ruleDir))
.filter((fileName) => fileName.endsWith('-metadata.json'))
.map((fileName) => (fileName.split('-')[0]));
const record = buildOneRuleRecord(allLanguages, rulesPath, ruleDir);
if (allLanguages.length < 1) {
logger.error(`No languages found for rule ${ruleDir}, at least 1 is required`);
return null;
}
if (types.size !== 1) {
logger.error(`${types.size} type(s) found for rule ${ruleDir}, 1 is required: ${JSON.stringify(types)}`);
if (record.types.size !== 1) {
logger.error(
`${record.types.size} type(s) found for rule ${ruleDir}, 1 is required: ${JSON.stringify(record.types)}`);
return null;
}
if (severities.size < 1) {
if (record.severities.size < 1) {
logger.error(`No severity found for rule ${ruleDir}, at least 1 is required`);
return null;
}
const indexedRecord: IndexedRuleWithDescription = {
id: ruleDir,
languages: allLanguages.sort(),
type: types.values().next().value,
severities: Array.from(severities).sort(),
all_keys: Array.from(all_keys).sort(),
titles: Array.from(titles).sort(),
tags: Array.from(tags).sort(),
qualityProfiles: Array.from(qualityProfiles).sort(),
descriptions: Array.from(descriptions).sort(),
prUrl
languages: Array.from(record.supportedLanguages).sort(),
type: record.types.values().next().value,
severities: Array.from(record.severities).sort(),
all_keys: Array.from(record.allKeys).sort(),
titles: Array.from(record.titles).sort(),
tags: Array.from(record.tags).sort(),
qualityProfiles: Array.from(record.qualityProfiles).sort(),
descriptions: Array.from(record.descriptions).sort(),
prUrl: record.prUrl
}
return [ruleDir, indexedRecord];
}
function buildIndexAggregates(indexedRecords: [string, IndexedRuleWithDescription][]): IndexAggregates {
const aggregates: IndexAggregates = { langs: {}, tags: {}, qualityProfiles: {} };
indexedRecords.forEach(record => {
record[1].qualityProfiles.forEach((qualityProfile) => {
if (qualityProfile in aggregates.qualityProfiles) {
aggregates.qualityProfiles[qualityProfile] += 1;
} else {
aggregates.qualityProfiles[qualityProfile] = 1;
}
});
record[1].languages.forEach(lang => {
if (lang.name in aggregates.langs) {
aggregates.langs[lang.name] += 1;
} else {
aggregates.langs[lang.name] = 1;
}
});
record[1].tags.forEach((tag) => {
if (tag in aggregates.tags) {
aggregates.tags[tag] += 1;
} else {
aggregates.tags[tag] = 1;
}
});
});
return aggregates;
}
/**
* Create the index store. This store is indexed by lunr and later used by the frontend.
* Whenever the lunr index finds something it returns IDs. The frontend will look at
* metadata corresponding to this ID in the index store.
* @param rulesPath Path to the directory containing aggregated metadata and rules
* descriptions in HTML format.
*/
export function buildIndexStore(rulesPath: string):[Record<string,IndexedRuleWithDescription>, IndexAggregates] {
const ruleDirs = fs.readdirSync(rulesPath).filter((fileName) => {
const fullpath = path.join(rulesPath, fileName);
return fs.lstatSync(fullpath).isDirectory();
});
const indexedRecords = ruleDirs.map((ruleDir) => buildOneRuleIndexedRecord(rulesPath, ruleDir));
const filteredRecords = indexedRecords.filter((value) => value !== null) as [string, IndexedRuleWithDescription][];
return [Object.fromEntries(filteredRecords),
{langs: allLangs, tags: allTags, qualityProfiles: allQualityProfiles}];
return [Object.fromEntries(filteredRecords), buildIndexAggregates(filteredRecords)];
}
/**
@ -154,7 +182,7 @@ export function buildSearchIndex(ruleIndexStore: IndexStore) {
lunr.Pipeline.registerFunction(selectivePipeline, 'selectivePipeline');
var ruleIndex = lunr(function () {
return lunr(function () {
// Set our own token processing pipeline
this.pipeline.reset();
this.pipeline.add(selectivePipeline);
@ -175,8 +203,6 @@ export function buildSearchIndex(ruleIndexStore: IndexStore) {
this.add(transformedRecord);
}
})
return ruleIndex;
}
/**

View File

@ -27,7 +27,7 @@ export function getRulesDirectories(srcPath: string, dstPath: string, rules?: st
* List every language for which a rule has a specialization, i.e. a sub-directory.
* @param ruleDirectory the rule's source directory
*/
export function listSupportedLanguage(ruleDirectory: string) {
export function listSupportedLanguages(ruleDirectory: string): string[] {
return fs.readdirSync(ruleDirectory)
.filter(fileName => fs.lstatSync(path.join(ruleDirectory, fileName)).isDirectory())
.sort();

View File

@ -1,8 +1,10 @@
import {LanguageSupport} from './RuleMetadata';
export type Severity = 'Blocker'|'Critical'|'Major'|'Minor'|'Info';
export interface IndexedRule {
id: string;
languages: string[];
languages: LanguageSupport[];
// FIXME: type, defaultSeverity should never be null but the index generation has a bug
type: 'BUG'|'CODE_SMELL'|'VULNERABILITY'|'SECURITY_HOTSPOT';
severities: Severity[];

View File

@ -1,6 +1,13 @@
export type Status = 'ready' | 'beta' | 'closed' | 'deprecated' | 'superseded';
export interface LanguageSupport {
name: string,
status: Status
}
export default interface RuleMetadata {
title: string,
all_languages: string[],
languagesSupport: LanguageSupport[],
allKeys: string[],
branch: string,
prUrl?: string

View File

@ -1,4 +1,5 @@
import { useFetch } from './useFetch';
import { Status } from '../types/RuleMetadata';
type Version = string | { since: string, until: string };
type RuleCoverage = Record<string, Record<string, Version>>;
@ -77,7 +78,28 @@ export function useRuleCoverage() {
return ruleCoverageForSonarpediaKeys(allLanguageKeys, ruleKeys, mapper);
}
function ruleStateInAnalyzer(language: string, ruleKeys: string[]): 'covered' | 'targeted' | 'removed' {
type AnalyzerState = 'covered' | 'targeted' | 'removed' | 'closed' | 'deprecated';
function analyzerStateFromCoverageAndStatus(coverage: Version[], status: Status): AnalyzerState {
if (coverage.length > 0) {
if (coverage.some(version => typeof version === 'string')) {
// if there is at least one coverage with simple (string) type, rule is still part of analyzer
if (status === 'deprecated' || status === 'superseded') {
return 'deprecated';
} else {
return 'covered';
}
} else {
// all coverages keep an analyzer versions range which means the rule was removed
return 'removed';
}
} else if (status === 'closed') {
return 'closed';
} else {
return 'targeted';
}
}
function ruleStateInAnalyzer(language: string, ruleKeys: string[], status: Status): AnalyzerState {
const languageKeys = languageToSonarpedia.get(language);
if (!languageKeys || coveredRulesError || coveredRulesIsLoading) {
if (coveredRulesError) {
@ -98,15 +120,7 @@ export function useRuleCoverage() {
})
);
if (result.length > 0) {
// if there is at least one entry with simple (string) type, rule is still part of analyzer
// otherwise (when all entries keep an analyzer versions range) the rule is removed
return result.some(version => typeof version === 'string')
? 'covered'
: 'removed';
} else {
return 'targeted';
}
return analyzerStateFromCoverageAndStatus(result, status);
}
return {ruleCoverage, allLangsRuleCoverage, ruleStateInAnalyzer};
@ -119,13 +133,23 @@ export const RULE_STATE = {
'darker': '#25699d'
},
'targeted': {
// orange
'color': '#FD7D20',
'darker': '#E26003'
// blueish green
'color': '#8aa8a6',
'darker': '#6c9390'
},
'removed': {
// red
'color': '#C72B28',
'darker': '#8D1B19'
},
'deprecated' : {
// orange
'color': '#FD7D20',
'darker': '#E26003'
},
'closed' : {
// dark grey
'color': '#505050',
'darker': '#202020'
}
}