diff --git a/frontend/sonar-project.properties b/frontend/sonar-project.properties index 78b1d80a8b..4bd46c0a3c 100644 --- a/frontend/sonar-project.properties +++ b/frontend/sonar-project.properties @@ -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/** diff --git a/frontend/src/RulePage.tsx b/frontend/src/RulePage.tsx index 3177e7534c..656f1e5475 100644 --- a/frontend/src/RulePage.tsx +++ b/frontend/src/RulePage.tsx @@ -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(descUrl, false); let [metadataJSON, metadataError, metadataIsLoading] = useFetch(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 ; + return ; }); metadataJSONString = JSON.stringify(metadataJSON, null, 2); const coverageMapper = (key: any, range: any) => { - if (typeof range === "string") { + if (typeof range === 'string') { return (
  • {key}: {range}
  • ); @@ -238,7 +248,7 @@ export function RulePage(props: any) { } } - if (coverage !== "Not Covered") { + if (coverage !== 'Not Covered') { prUrl = undefined; branch = 'master'; } diff --git a/frontend/src/SearchHit.tsx b/frontend/src/SearchHit.tsx index 722fc2e987..1cf5dd3fb0 100644 --- a/frontend/src/SearchHit.tsx +++ b/frontend/src/SearchHit.tsx @@ -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,27 +107,40 @@ 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 = ; - if (ruleState === 'covered') { - coveredLanguages.push(chip); - } else if (ruleState === 'targeted') { - targetedLanguages.push(chip); - } else { - removedLanguages.push(chip); - } + switch(ruleState) { + case 'targeted': + targetedLanguages.push(chip); + 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} ; + const deprecatedBlock = deprecatedLanguages.length === 0 ? <> + : + + {deprecatedLanguages} + ; + const closedBlock = closedLanguages.length === 0 ? <> + : + + {closedLanguages} + ; + return ( @@ -137,6 +189,8 @@ export function SearchHit(props: SearchHitProps) { {coveredBlock} {targetedBlock} {removedBlock} + {deprecatedBlock} + {closedBlock} ) diff --git a/frontend/src/__tests__/RulePage.test.tsx b/frontend/src/__tests__/RulePage.test.tsx index 7bdc9a09da..43aca8547a 100644 --- a/frontend/src/__tests__/RulePage.test.tsx +++ b/frontend/src/__tests__/RulePage.test.tsx @@ -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(); diff --git a/frontend/src/__tests__/SearchPage.test.tsx b/frontend/src/__tests__/SearchPage.test.tsx index 539434fc61..4870ec5e7b 100644 --- a/frontend/src/__tests__/SearchPage.test.tsx +++ b/frontend/src/__tests__/SearchPage.test.tsx @@ -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)); }); diff --git a/frontend/src/__tests__/__snapshots__/RulePage.test.tsx.snap b/frontend/src/__tests__/__snapshots__/RulePage.test.tsx.snap index a46d20c6f6..d2a08f4dbb 100644 --- a/frontend/src/__tests__/__snapshots__/RulePage.test.tsx.snap +++ b/frontend/src/__tests__/__snapshots__/RulePage.test.tsx.snap @@ -4,33 +4,33 @@ exports[`renders C# version of S3457 (using GH for issues instead of Jira) 1`] =

    S3457

    @@ -114,12 +114,12 @@ exports[`renders C# version of S3457 (using GH for issues instead of Jira) 1`] = class="MuiContainer-root MuiContainer-maxWidthMd" >

    Composite format strings should be used correctly

    @@ -574,6 +574,66 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o ], + + + + "allKeys" + + + + : [ + + + + + "S3457" + + + + + ], + + + + + "branch" + + + + : + + + + "master" + + + + , + @@ -603,6 +663,81 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , + + + + "extra" + + + + : { + + + + + "legacyKeys" + + + + : [], + + + + + "replacementRules" + + + + : [] + }, + + + + + "quickfix" + + + + : + + + + "unknown" + + + + , + @@ -699,12 +834,27 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - "all_languages" + "languagesSupport" : [ - + { + + + + + "name" + + + + : @@ -719,7 +869,52 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , - + + + + + "status" + + + + : + + + + "ready" + + + + + }, + { + + + + + "name" + + + + : @@ -734,7 +929,52 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , - + + + + + "status" + + + + : + + + + "ready" + + + + + }, + { + + + + + "name" + + + + : @@ -749,7 +989,52 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , - + + + + + "status" + + + + : + + + + "closed" + + + + + }, + { + + + + + "name" + + + + : @@ -763,9 +1048,8 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - - ], - + , + @@ -775,12 +1059,11 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - "allKeys" + "status" - : [ - + : @@ -790,11 +1073,12 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - "S3457" + "deprecated" + } ] } @@ -862,7 +1146,7 @@ exports[`renders cfamily version of S1000 1`] = `
    @@ -878,7 +1162,7 @@ exports[`renders cfamily version of S1000 1`] = ` Header files should not contain unnamed namespaces

    , + + + + "quickfix" + + + + : + + + + "unknown" + + + + , + @@ -1418,6 +1731,21 @@ void fn_b(void) , + + + + "clumsy" + + + + , + @@ -1458,7 +1786,7 @@ void fn_b(void) - "coveredLanguages" + "legacyKeys" @@ -1473,22 +1801,7 @@ void fn_b(void) - "C++" - - - - , - - - - - "C" + "UnnamedNamespaceInHeader" @@ -1627,6 +1940,53 @@ void fn_b(void) , + + + + "securityStandards" + + + + : { + + + + + "CERT" + + + + : [ + + + + + "DCL59-CPP." + + + + + ] + }, + @@ -1682,12 +2042,27 @@ void fn_b(void) - "all_languages" + "languagesSupport" : [ - + { + + + + + "name" + + + + : @@ -1701,7 +2076,37 @@ void fn_b(void) + , + + + + + "status" + + + + : + + + + "ready" + + + + } ],

    S3457

    @@ -1888,12 +2293,12 @@ exports[`renders generic version of S3457 1`] = ` class="MuiContainer-root MuiContainer-maxWidthMd" >

    Composite format strings should be used correctly

    @@ -2342,6 +2747,66 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o ], + + + + "allKeys" + + + + : [ + + + + + "S3457" + + + + + ], + + + + + "branch" + + + + : + + + + "master" + + + + , + @@ -2371,6 +2836,81 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , + + + + "extra" + + + + : { + + + + + "legacyKeys" + + + + : [], + + + + + "replacementRules" + + + + : [] + }, + + + + + "quickfix" + + + + : + + + + "unknown" + + + + , + @@ -2467,12 +3007,27 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - "all_languages" + "languagesSupport" : [ - + { + + + + + "name" + + + + : @@ -2487,7 +3042,52 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , - + + + + + "status" + + + + : + + + + "ready" + + + + + }, + { + + + + + "name" + + + + : @@ -2502,7 +3102,52 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , - + + + + + "status" + + + + : + + + + "ready" + + + + + }, + { + + + + + "name" + + + + : @@ -2517,7 +3162,52 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o , - + + + + + "status" + + + + : + + + + "closed" + + + + + }, + { + + + + + "name" + + + + : @@ -2531,9 +3221,8 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - - ], - + , + @@ -2543,12 +3232,11 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - "allKeys" + "status" - : [ - + : @@ -2558,11 +3246,12 @@ var res = string.Format("{0} {1}", array); // Compliant we don't know the size o - "S3457" + "deprecated" + } ] } diff --git a/frontend/src/__tests__/__snapshots__/SearchPage.test.tsx.snap b/frontend/src/__tests__/__snapshots__/SearchPage.test.tsx.snap index 63c3bb4c5c..8eedaf5b67 100644 --- a/frontend/src/__tests__/__snapshots__/SearchPage.test.tsx.snap +++ b/frontend/src/__tests__/__snapshots__/SearchPage.test.tsx.snap @@ -341,12 +341,12 @@ exports[`renders the list of all rules 1`] = ` class="MuiTypography-root makeStyles-language-16 MuiTypography-body2" >
    - Targeted + Covered
    @@ -374,7 +374,7 @@ exports[`renders the list of all rules 1`] = `
    - Targeted + Covered
    diff --git a/frontend/src/deployment/__tests__/description.test.ts b/frontend/src/deployment/__tests__/description.test.ts index cfc37133d8..c2b2456f41 100644 --- a/frontend/src/deployment/__tests__/description.test.ts +++ b/frontend/src/deployment/__tests__/description.test.ts @@ -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( diff --git a/frontend/src/deployment/__tests__/metadata.test.ts b/frontend/src/deployment/__tests__/metadata.test.ts index 6175b1e182..92dde785dd 100644 --- a/frontend/src/deployment/__tests__/metadata.test.ts +++ b/frontend/src/deployment/__tests__/metadata.test.ts @@ -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); + }); + }); }); diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S1000/cfamily-description.html b/frontend/src/deployment/__tests__/resources/metadata/S1000/cfamily-description.html similarity index 100% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S1000/cfamily-description.html rename to frontend/src/deployment/__tests__/resources/metadata/S1000/cfamily-description.html diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S1000/cfamily-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S1000/cfamily-metadata.json similarity index 70% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S1000/cfamily-metadata.json rename to frontend/src/deployment/__tests__/resources/metadata/S1000/cfamily-metadata.json index b2a04d6d70..7852a2b12b 100644 --- a/frontend/src/deployment/__tests__/resources/plugin_rules/S1000/cfamily-metadata.json +++ b/frontend/src/deployment/__tests__/resources/metadata/S1000/cfamily-metadata.json @@ -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" diff --git a/frontend/src/deployment/__tests__/resources/metadata/S1000/default-description.html b/frontend/src/deployment/__tests__/resources/metadata/S1000/default-description.html new file mode 100644 index 0000000000..693bd83eda --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S1000/default-description.html @@ -0,0 +1,85 @@ +
    +

    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
    +  {
    +  }
    +}
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    (visible only on this page)

    +
    +
    +

    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

    +
    +
    +
    +
    \ No newline at end of file diff --git a/frontend/src/deployment/__tests__/resources/metadata/S1000/default-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S1000/default-metadata.json new file mode 100644 index 0000000000..449366c8d3 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S1000/default-metadata.json @@ -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" +} \ No newline at end of file diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/cfamily-description.html b/frontend/src/deployment/__tests__/resources/metadata/S3457/cfamily-description.html similarity index 100% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S3457/cfamily-description.html rename to frontend/src/deployment/__tests__/resources/metadata/S3457/cfamily-description.html diff --git a/frontend/src/deployment/__tests__/resources/metadata/S3457/cfamily-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S3457/cfamily-metadata.json new file mode 100644 index 0000000000..5be9c0f54d --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S3457/cfamily-metadata.json @@ -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" + } + ] +} diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/csharp-description.html b/frontend/src/deployment/__tests__/resources/metadata/S3457/csharp-description.html similarity index 100% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S3457/csharp-description.html rename to frontend/src/deployment/__tests__/resources/metadata/S3457/csharp-description.html diff --git a/frontend/src/deployment/__tests__/resources/metadata/S3457/csharp-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S3457/csharp-metadata.json new file mode 100644 index 0000000000..be874bbd29 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S3457/csharp-metadata.json @@ -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" + } + ] +} diff --git a/frontend/src/deployment/__tests__/resources/metadata/S3457/default-description.html b/frontend/src/deployment/__tests__/resources/metadata/S3457/default-description.html new file mode 100644 index 0000000000..907ec91d60 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S3457/default-description.html @@ -0,0 +1,92 @@ +
    +

    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

    +
    +
    + +
    +
    +
    +
    +
    +

    Implementation Specification

    +
    +
    +

    (visible only on this page)

    +
    +
    +

    Message

    +
    +

    XXXX

    +
    +
    +
    +
    +
    +
    + +
    +
    +

    (visible only on this page)

    +
    +
    +

    is duplicated by: S3941

    + +
    +
    + + +
    +
    +

    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]

    +
    +
    +
    +
    \ No newline at end of file diff --git a/frontend/src/deployment/__tests__/resources/metadata/S3457/default-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S3457/default-metadata.json new file mode 100644 index 0000000000..17039a58b8 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S3457/default-metadata.json @@ -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" +} \ No newline at end of file diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/java-description.html b/frontend/src/deployment/__tests__/resources/metadata/S3457/java-description.html similarity index 100% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S3457/java-description.html rename to frontend/src/deployment/__tests__/resources/metadata/S3457/java-description.html diff --git a/frontend/src/deployment/__tests__/resources/metadata/S3457/java-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S3457/java-metadata.json new file mode 100644 index 0000000000..e093e8d891 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S3457/java-metadata.json @@ -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" + } + ] +} diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/python-description.html b/frontend/src/deployment/__tests__/resources/metadata/S3457/python-description.html similarity index 100% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S3457/python-description.html rename to frontend/src/deployment/__tests__/resources/metadata/S3457/python-description.html diff --git a/frontend/src/deployment/__tests__/resources/metadata/S3457/python-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S3457/python-metadata.json new file mode 100644 index 0000000000..3fd30c8172 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S3457/python-metadata.json @@ -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" + } + ] +} diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S987/cfamily-description.html b/frontend/src/deployment/__tests__/resources/metadata/S987/cfamily-description.html similarity index 100% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S987/cfamily-description.html rename to frontend/src/deployment/__tests__/resources/metadata/S987/cfamily-description.html diff --git a/frontend/src/deployment/__tests__/resources/metadata/S987/cfamily-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S987/cfamily-metadata.json new file mode 100644 index 0000000000..5e45492c94 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S987/cfamily-metadata.json @@ -0,0 +1,35 @@ +{ + "title": "\"\" 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" + } + ] +} diff --git a/frontend/src/deployment/__tests__/resources/metadata/S987/default-description.html b/frontend/src/deployment/__tests__/resources/metadata/S987/default-description.html new file mode 100644 index 0000000000..e2e7e73249 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S987/default-description.html @@ -0,0 +1,47 @@ +
    +

    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

      +
    • +
    +
    +
    +
    +
    +
    + +
    +
    +

    (visible only on this page)

    +
    +
    + + +
    +
    +

    on 31 Mar 2015, 19:07:23 Evgeny Mandrikov wrote:

    +
    +

    \[~ann.campbell.2] implementation seems more complete (SQALE, description) than this spec.

    +
    +
    +
    +
    \ No newline at end of file diff --git a/frontend/src/deployment/__tests__/resources/metadata/S987/default-metadata.json b/frontend/src/deployment/__tests__/resources/metadata/S987/default-metadata.json new file mode 100644 index 0000000000..8c9f028c0b --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/metadata/S987/default-metadata.json @@ -0,0 +1,35 @@ +{ + "title": "\"\" 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" +} \ No newline at end of file diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/java-metadata.json b/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/java-metadata.json deleted file mode 100644 index 0069c96c65..0000000000 --- a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/java-metadata.json +++ /dev/null @@ -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" -} diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/python-metadata.json b/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/python-metadata.json deleted file mode 100644 index 3ce2b99e01..0000000000 --- a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/python-metadata.json +++ /dev/null @@ -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" -} diff --git a/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/comments-and-links.adoc b/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/comments-and-links.adoc new file mode 100644 index 0000000000..46cd0a52a1 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/comments-and-links.adoc @@ -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 + + + diff --git a/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/metadata.json b/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/metadata.json new file mode 100644 index 0000000000..a479a73886 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/metadata.json @@ -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" +} diff --git a/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/rule.adoc new file mode 100644 index 0000000000..cb59251e3e --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S1000/cfamily/rule.adoc @@ -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[] diff --git a/frontend/src/deployment/__tests__/resources/rules/S1000/metadata.json b/frontend/src/deployment/__tests__/resources/rules/S1000/metadata.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S1000/metadata.json @@ -0,0 +1,2 @@ +{ +} diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/cfamily-metadata.json b/frontend/src/deployment/__tests__/resources/rules/S3457/cfamily/metadata.json similarity index 53% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S3457/cfamily-metadata.json rename to frontend/src/deployment/__tests__/resources/rules/S3457/cfamily/metadata.json index b2fe3504c2..3784ba9912 100644 --- a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/cfamily-metadata.json +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/cfamily/metadata.json @@ -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." + ] + } } diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/cfamily/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/cfamily/rule.adoc new file mode 100644 index 0000000000..f311bc9cf4 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/cfamily/rule.adoc @@ -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[] diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/comments-and-links.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/comments-and-links.adoc new file mode 100644 index 0000000000..8d038ae2a2 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/comments-and-links.adoc @@ -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] + diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/compliant.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/compliant.adoc new file mode 100644 index 0000000000..5b23b2700c --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/compliant.adoc @@ -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); +---- diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/csharp/metadata.json b/frontend/src/deployment/__tests__/resources/rules/S3457/csharp/metadata.json new file mode 100644 index 0000000000..7ff07da58b --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/csharp/metadata.json @@ -0,0 +1,3 @@ +{ + "title": "Composite format strings should be used correctly" +} diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/csharp/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/csharp/rule.adoc new file mode 100644 index 0000000000..f2820cac56 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/csharp/rule.adoc @@ -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[] diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/description.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/description.adoc new file mode 100644 index 0000000000..bc5966d632 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/description.adoc @@ -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. diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/java/metadata.json b/frontend/src/deployment/__tests__/resources/rules/S3457/java/metadata.json new file mode 100644 index 0000000000..ebe3b2559d --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/java/metadata.json @@ -0,0 +1,12 @@ +{ + "tags": [ + "cert", + "confusing" + ], + "status": "closed", + "securityStandards": { + "CERT": [ + "FIO47-C." + ] + } +} diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/java/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/java/rule.adoc new file mode 100644 index 0000000000..3225ffc888 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/java/rule.adoc @@ -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[] diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/message.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/message.adoc new file mode 100644 index 0000000000..5af8d537c9 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/message.adoc @@ -0,0 +1,4 @@ +=== Message + +XXXX + diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/csharp-metadata.json b/frontend/src/deployment/__tests__/resources/rules/S3457/metadata.json similarity index 82% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S3457/csharp-metadata.json rename to frontend/src/deployment/__tests__/resources/rules/S3457/metadata.json index 6a3a2e981a..27a4994f45 100644 --- a/frontend/src/deployment/__tests__/resources/plugin_rules/S3457/csharp-metadata.json +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/metadata.json @@ -16,13 +16,12 @@ "ruleSpecification": "RSPEC-3457", "sqKey": "S3457", "scope": "All", - "all_languages": [ - "cfamily", - "csharp", - "java", - "python" - ], "allKeys": [ "S3457" - ] + ], + "extra": { + "replacementRules": [], + "legacyKeys": [] + }, + "quickfix": "unknown" } diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/noncompliant.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/noncompliant.adoc new file mode 100644 index 0000000000..b85938a03e --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/noncompliant.adoc @@ -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. +---- diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/python/message.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/python/message.adoc new file mode 100644 index 0000000000..9b31a8ebc5 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/python/message.adoc @@ -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. + + diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/python/metadata.json b/frontend/src/deployment/__tests__/resources/rules/S3457/python/metadata.json new file mode 100644 index 0000000000..fc663ab102 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/python/metadata.json @@ -0,0 +1,4 @@ +{ + "title": "String formatting should be used correctly", + "status": "deprecated" +} diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/python/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/python/rule.adoc new file mode 100644 index 0000000000..ebfd2f1bb6 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/python/rule.adoc @@ -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[] diff --git a/frontend/src/deployment/__tests__/resources/rules/S3457/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S3457/rule.adoc new file mode 100644 index 0000000000..4cd2d530fd --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S3457/rule.adoc @@ -0,0 +1,6 @@ +include::description.adoc[] + +include::noncompliant.adoc[] + +include::compliant.adoc[] + diff --git a/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/comments-and-links.adoc b/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/comments-and-links.adoc new file mode 100644 index 0000000000..1778b6e08d --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/comments-and-links.adoc @@ -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. + diff --git a/frontend/src/deployment/__tests__/resources/plugin_rules/S987/cfamily-metadata.json b/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/metadata.json similarity index 64% rename from frontend/src/deployment/__tests__/resources/plugin_rules/S987/cfamily-metadata.json rename to frontend/src/deployment/__tests__/resources/rules/S987/cfamily/metadata.json index fa6d2a51d0..d3f66e0cdc 100644 --- a/frontend/src/deployment/__tests__/resources/plugin_rules/S987/cfamily-metadata.json +++ b/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/metadata.json @@ -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" } diff --git a/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/rule.adoc b/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/rule.adoc new file mode 100644 index 0000000000..20190d3020 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S987/cfamily/rule.adoc @@ -0,0 +1,23 @@ +Signal handling contains implementation-defined and undefined behavior. + + +== Noncompliant Code Example + +---- +#include /* Noncompliant */ +---- + + +== See + +* MISRA C:2004, 20.8 - The signal handling facilities of shall not be used. +* MISRA C:2012, 21.5 - The standard header file 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[] diff --git a/frontend/src/deployment/__tests__/resources/rules/S987/metadata.json b/frontend/src/deployment/__tests__/resources/rules/S987/metadata.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/frontend/src/deployment/__tests__/resources/rules/S987/metadata.json @@ -0,0 +1,2 @@ +{ +} diff --git a/frontend/src/deployment/__tests__/searchIndex.test.ts b/frontend/src/deployment/__tests__/searchIndex.test.ts index 08ad5a4f16..34769d1366 100644 --- a/frontend/src/deployment/__tests__/searchIndex.test.ts +++ b/frontend/src/deployment/__tests__/searchIndex.test.ts @@ -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']); diff --git a/frontend/src/deployment/description.ts b/frontend/src/deployment/description.ts index 94c4d209b7..a679d008fe 100644 --- a/frontend/src/deployment/description.ts +++ b/frontend/src/deployment/description.ts @@ -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)); diff --git a/frontend/src/deployment/index.ts b/frontend/src/deployment/index.ts index 54668dfb6e..2e2b8e9e9a 100755 --- a/frontend/src/deployment/index.ts +++ b/frontend/src/deployment/index.ts @@ -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('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('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); }); diff --git a/frontend/src/deployment/metadata.ts b/frontend/src/deployment/metadata.ts index 8b624a7c6a..e0c9908952 100644 --- a/frontend/src/deployment/metadata.ts +++ b/frontend/src/deployment/metadata.ts @@ -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, - branch: string, prUrl?: 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}; } diff --git a/frontend/src/deployment/searchIndex.ts b/frontend/src/deployment/searchIndex.ts index 560ef001b7..0789db0f6b 100644 --- a/frontend/src/deployment/searchIndex.ts +++ b/frontend/src/deployment/searchIndex.ts @@ -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,6 +18,133 @@ export interface IndexedRuleWithDescription extends IndexedRule { descriptions?: Array; } +function buildOneRuleRecord(allLanguages: string[], rulesPath: string, ruleDir: string) { + + let types = new Set(); + let severities = new Set(); + const allKeys = new Set([ruleDir]); + const titles = new Set(); + const tags = new Set(); + const qualityProfiles = new Set(); + const descriptions = new Set(); + const supportedLanguages : LanguageSupport[] = []; + let prUrl = undefined; + + allLanguages.forEach((lang) => { + // extract every word of every description of this rule + const descriptionPath = path.join(rulesPath, ruleDir, `${lang}-description.html`); + const descriptionStr = fs.readFileSync(descriptionPath).toString(); + // Remove HTML tags from the description, extract unique words and normalize them. + // This reduces a bit the footprint of descriptions in the index. + const descriptionWords = stripHtml(descriptionStr).result.split(DESCRIPTION_SPLIT_REGEX) + descriptionWords.forEach((word) => descriptions.add(word)); + + // merge metadata fields of every version of this rule in a single indexed record + const metadataPath = path.join(rulesPath, ruleDir, `${lang}-metadata.json`); + const metadataStr = fs.readFileSync(metadataPath).toString(); + const metadata = JSON.parse(metadataStr); + + if (metadata.prUrl) { + prUrl = metadata.prUrl; + } + 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); + } + } + if (metadata.defaultQualityProfiles) { + for (const qualityProfile of metadata.defaultQualityProfiles) { + qualityProfiles.add(qualityProfile); + } + } + }); + 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 (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 (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: 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 @@ -25,113 +153,13 @@ export interface IndexedRuleWithDescription extends IndexedRule { * descriptions in HTML format. */ export function buildIndexStore(rulesPath: string):[Record, IndexAggregates] { - let ruleDirs = fs.readdirSync(rulesPath).filter((fileName) => { + const 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]); - - let types = new Set(); - let severities = new Set(); - const all_keys = new Set([ruleDir]); - const titles = new Set(); - const tags = new Set(); - const qualityProfiles = new Set(); - const descriptions = new Set(); - let prUrl = undefined; - - allLanguages.forEach((lang) => { - // extract every word of every description of this rule - const descriptionPath = path.join(rulesPath, ruleDir, `${lang}-description.html`); - const descriptionStr = fs.readFileSync(descriptionPath).toString(); - // Remove HTML tags from the description, extract unique words and normalize them. - // This reduces a bit the footprint of descriptions in the index. - const descriptionWords = stripHtml(descriptionStr).result.split(DESCRIPTION_SPLIT_REGEX) - descriptionWords.forEach((word) => descriptions.add(word)); - - // merge metadata fields of every version of this rule in a single indexed record - const metadataPath = path.join(rulesPath, ruleDir, `${lang}-metadata.json`); - const metadataStr = fs.readFileSync(metadataPath).toString(); - const metadata = JSON.parse(metadataStr); - - if (metadata.prUrl) { - prUrl = metadata.prUrl; - } - all_keys.add(metadata.sqKey); - all_keys.add(metadata.ruleSpecification); - titles.add(metadata.title); - types.add(metadata.type); - severities.add(metadata.defaultSeverity as Severity); - if (metadata.tags) { - for (const tag of metadata.tags) { - tags.add(tag); - } - } - if (metadata.defaultQualityProfiles) { - for (const qualityProfile of metadata.defaultQualityProfiles) { - 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; - } - }); - - 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)}`); - return null; - } - if (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 - } - - return [ruleDir, indexedRecord]; - }); - + 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; } /** diff --git a/frontend/src/deployment/utils.ts b/frontend/src/deployment/utils.ts index 924877c9c0..984523631d 100644 --- a/frontend/src/deployment/utils.ts +++ b/frontend/src/deployment/utils.ts @@ -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(); diff --git a/frontend/src/types/IndexStore.ts b/frontend/src/types/IndexStore.ts index 68047c5636..e04109b723 100644 --- a/frontend/src/types/IndexStore.ts +++ b/frontend/src/types/IndexStore.ts @@ -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[]; diff --git a/frontend/src/types/RuleMetadata.ts b/frontend/src/types/RuleMetadata.ts index dc378381ac..fa64254814 100644 --- a/frontend/src/types/RuleMetadata.ts +++ b/frontend/src/types/RuleMetadata.ts @@ -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 diff --git a/frontend/src/utils/useRuleCoverage.ts b/frontend/src/utils/useRuleCoverage.ts index 1536e166ef..ed7eb8cd77 100644 --- a/frontend/src/utils/useRuleCoverage.ts +++ b/frontend/src/utils/useRuleCoverage.ts @@ -1,4 +1,5 @@ import { useFetch } from './useFetch'; +import { Status } from '../types/RuleMetadata'; type Version = string | { since: string, until: string }; type RuleCoverage = Record>; @@ -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' } }