Skip to content

Commit

Permalink
Refactor tools/helper
Browse files Browse the repository at this point in the history
  • Loading branch information
zglicz authored and vdiez committed Mar 7, 2025
1 parent 0c56896 commit 91f400e
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 323 deletions.
2 changes: 1 addition & 1 deletion its/sources/jsts/projects
Submodule projects updated 3 files
+1 −1 .github/CODEOWNERS
+3 −0 README.md
+13 −0 SECURITY.md
71 changes: 71 additions & 0 deletions tools/generate-eslint-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/

import { join } from 'node:path';
import { defaultOptions } from '../packages/jsts/src/rules/helpers/configs.js';
import {
getESLintDefaultConfiguration,
getRspecMeta,
header,
inflateTemplateToFile,
METADATA_FOLDER,
RULES_FOLDER,
TS_TEMPLATES_FOLDER,
typeMatrix,
} from './helpers.js';
import { readFile } from 'fs/promises';

const sonarWayProfile = JSON.parse(
await readFile(join(METADATA_FOLDER, `Sonar_way_profile.json`), 'utf-8'),
);

/**
* From the RSPEC json file, creates a generated-meta.ts file with ESLint formatted metadata
*
* @param sonarKey rule ID for which we need to create the generated-meta.ts file
* @param defaults if rspec not found, extra properties to set. Useful for the new-rule script
*/
export async function generateMetaForRule(
sonarKey: string,
defaults?: { compatibleLanguages?: ('JAVASCRIPT' | 'TYPESCRIPT')[]; scope?: 'Main' | 'Tests' },
) {
const ruleRspecMeta = await getRspecMeta(sonarKey, defaults);
if (!typeMatrix[ruleRspecMeta.type]) {
console.log(`Type not found for rule ${sonarKey}`);
}

const ruleFolder = join(RULES_FOLDER, sonarKey);
const eslintConfiguration = await getESLintDefaultConfiguration(sonarKey);

await inflateTemplateToFile(
join(TS_TEMPLATES_FOLDER, 'generated-meta.template'),
join(ruleFolder, `generated-meta.ts`),
{
___HEADER___: header,
___RULE_TYPE___: typeMatrix[ruleRspecMeta.type],
___RULE_KEY___: sonarKey,
___DESCRIPTION___: ruleRspecMeta.title.replace(/'/g, "\\'"),
___RECOMMENDED___: sonarWayProfile.ruleKeys.includes(sonarKey),
___TYPE_CHECKING___: `${ruleRspecMeta.tags.includes('type-dependent')}`,
___FIXABLE___: ruleRspecMeta.quickfix === 'covered' ? "'code'" : undefined,
___DEPRECATED___: `${ruleRspecMeta.status === 'deprecated'}`,
___DEFAULT_OPTIONS___: JSON.stringify(defaultOptions(eslintConfiguration), null, 2),
___LANGUAGES___: JSON.stringify(ruleRspecMeta.compatibleLanguages),
___SCOPE___: ruleRspecMeta.scope,
},
);
}
243 changes: 243 additions & 0 deletions tools/generate-java-rule-classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* SonarQube JavaScript Plugin
* Copyright (C) 2011-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
import { join } from 'node:path';
import {
ESLintConfiguration,
ESLintConfigurationProperty,
ESLintConfigurationSQProperty,
} from '../packages/jsts/src/rules/helpers/configs.js';
import assert from 'node:assert';
import {
getESLintDefaultConfiguration,
getRspecMeta,
header,
inflateTemplateToFile,
JAVA_TEMPLATES_FOLDER,
REPOSITORY_ROOT,
} from './helpers.js';

const JAVA_CHECKS_FOLDER = join(
REPOSITORY_ROOT,
'sonar-plugin',
'javascript-checks',
'src',
'main',
'java',
'org',
'sonar',
'javascript',
'checks',
);

export async function generateParsingErrorClass() {
await inflateTemplateToFile(
join(JAVA_TEMPLATES_FOLDER, 'parsingError.template'),
join(JAVA_CHECKS_FOLDER, `S2260.java`),
{
___HEADER___: header,
},
);
}

async function inflate1541() {
await inflateTemplateToFile(
join(JAVA_TEMPLATES_FOLDER, 'S1541.template'),
join(JAVA_CHECKS_FOLDER, `S1541Ts.java`),
{
___HEADER___: header,
___DECORATOR___: 'TypeScriptRule',
___CLASS_NAME___: 'S1541Ts',
___SQ_PROPERTY_NAME___: 'Threshold',
___SQ_PROPERTY_DESCRIPTION___: 'The maximum authorized complexity.',
},
);
await inflateTemplateToFile(
join(JAVA_TEMPLATES_FOLDER, 'S1541.template'),
join(JAVA_CHECKS_FOLDER, `S1541Js.java`),
{
___HEADER___: header,
___DECORATOR___: 'JavaScriptRule',
___CLASS_NAME___: 'S1541Js',
___SQ_PROPERTY_NAME___: 'maximumFunctionComplexityThreshold',
___SQ_PROPERTY_DESCRIPTION___: 'The maximum authorized complexity in function',
},
);
}

export async function generateJavaCheckClass(
sonarKey: string,
defaults?: { compatibleLanguages?: ('JAVASCRIPT' | 'TYPESCRIPT')[]; scope?: 'Main' | 'Tests' },
) {
if (sonarKey === 'S1541') {
await inflate1541();
return;
}
const ruleRspecMeta = await getRspecMeta(sonarKey, defaults);
const imports: Set<string> = new Set();
const decorators = [];
let javaCheckClass: string;
if (ruleRspecMeta.scope === 'Tests') {
javaCheckClass = 'TestFileCheck';
imports.add('import org.sonar.plugins.javascript.api.TestFileCheck;');
} else {
javaCheckClass = 'Check';
imports.add('import org.sonar.plugins.javascript.api.Check;');
}

const derivedLanguages = ruleRspecMeta.compatibleLanguages;
if (derivedLanguages.includes('JAVASCRIPT')) {
decorators.push('@JavaScriptRule');
imports.add('import org.sonar.plugins.javascript.api.JavaScriptRule;');
}
if (derivedLanguages.includes('TYPESCRIPT')) {
decorators.push('@TypeScriptRule');
imports.add('import org.sonar.plugins.javascript.api.TypeScriptRule;');
}

const eslintConfiguration = await getESLintDefaultConfiguration(sonarKey);
const body = generateBody(eslintConfiguration, imports);

await inflateTemplateToFile(
join(JAVA_TEMPLATES_FOLDER, 'check.template'),
join(JAVA_CHECKS_FOLDER, `${sonarKey}.java`),
{
___HEADER___: header,
___DECORATORS___: decorators.join('\n'),
___RULE_KEY___: sonarKey,
___FILE_TYPE_CHECK___: javaCheckClass,
___IMPORTS___: [...imports].join('\n'),
___BODY___: body.join('\n'),
},
);
}

function isSonarSQProperty(
property: ESLintConfigurationProperty,
): property is ESLintConfigurationSQProperty {
return (property as ESLintConfigurationSQProperty).description !== undefined;
}

function generateBody(config: ESLintConfiguration, imports: Set<string>) {
const result = [];
let hasSQProperties = false;

function generateRuleProperty(property: ESLintConfigurationProperty) {
if (!isSonarSQProperty(property)) {
return;
}

const getSQDefault = () => {
return property.customDefault ?? property.default;
};

const getJavaType = () => {
const defaultValue = getSQDefault();
switch (typeof defaultValue) {
case 'number':
return 'int';
case 'string':
return 'String';
case 'boolean':
return 'boolean';
default:
return 'String';
}
};

const getDefaultValueString = () => {
const defaultValue = getSQDefault();
switch (typeof defaultValue) {
case 'number':
case 'boolean':
return `"" + ${defaultValue}`;
case 'string':
return `"${defaultValue}"`;
case 'object': {
assert(Array.isArray(defaultValue));
return `"${defaultValue.join(',')}"`;
}
}
};

const getDefaultValue = () => {
const defaultValue = getSQDefault();
switch (typeof defaultValue) {
case 'number':
case 'boolean':
return `${defaultValue.toString()}`;
case 'string':
return `"${defaultValue}"`;
case 'object':
assert(Array.isArray(defaultValue));
return `"${defaultValue.join(',')}"`;
}
};

const defaultFieldName = 'field' in property ? (property.field as string) : 'value';
const defaultValue = getDefaultValueString();
imports.add('import org.sonar.check.RuleProperty;');
result.push(
`@RuleProperty(key="${property.displayName ?? defaultFieldName}", description = "${property.description}", defaultValue = ${defaultValue})`,
);
result.push(`${getJavaType()} ${defaultFieldName} = ${getDefaultValue()};`);
hasSQProperties = true;
return defaultFieldName;
}

const configurations = [];
config.forEach(config => {
if (Array.isArray(config)) {
const fields = config
.map(namedProperty => {
const fieldName = generateRuleProperty(namedProperty);
if (!isSonarSQProperty(namedProperty) || !fieldName) {
return undefined;
}
let value: string;
if (typeof namedProperty.default === 'object') {
const castTo = namedProperty.items.type === 'string' ? 'String' : 'Integer';
imports.add('import java.util.Arrays;');
value = `Arrays.stream(${fieldName}.split(",")).map(String::trim).toArray(${castTo}[]::new)`;
} else if (namedProperty.customForConfiguration) {
value = namedProperty.customForConfiguration;
} else {
value = fieldName;
}
return { fieldName, value };
})
.filter(field => field);
if (fields.length > 0) {
imports.add('import java.util.Map;');
const mapContents = fields.map(({ fieldName, value }) => `"${fieldName}", ${value}`);
configurations.push(`Map.of(${mapContents})`);
}
} else {
let value = generateRuleProperty(config);
if (isSonarSQProperty(config) && config.customForConfiguration) {
value = config.customForConfiguration;
}
configurations.push(value);
}
});
if (hasSQProperties) {
imports.add('import java.util.List;');
result.push(
`@Override\npublic List<Object> configurations() {\n return List.of(${configurations.join(',')});\n}\n`,
);
}
return result;
}
15 changes: 6 additions & 9 deletions tools/generate-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,19 @@
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
import {
generateMetaForRule,
listRulesDir,
generateJavaCheckClass,
generateParsingErrorClass,
} from './helpers.js';
import { listRulesDir } from './helpers.js';
import { generateMetaForRule } from './generate-eslint-meta.js';
import { generateJavaCheckClass, generateParsingErrorClass } from './generate-java-rule-classes.js';
import { updateIndexes } from './generate-rule-indexes.js';

/**
* Generate packages/jsts/src/rules/SXXXX/generated-meta.ts on each rule
* with data coming from the RSPEC json files. This data fills in the Rule ESLint metadata
* as well as the JSON schema files available in "packages/jsts/src/rules/SXXXX/schema.json"
* Also, generate SXXX Java Check classes.
*/
for (const file of await listRulesDir()) {
await generateMetaForRule(file);
await generateJavaCheckClass(file);
}
await generateParsingErrorClass();

await import('./generate-rule-indexes.js');
await updateIndexes();
Loading

0 comments on commit 91f400e

Please sign in to comment.