diff --git a/its/sources/jsts/projects b/its/sources/jsts/projects index 28ac1d64d6b..2c333561bf3 160000 --- a/its/sources/jsts/projects +++ b/its/sources/jsts/projects @@ -1 +1 @@ -Subproject commit 28ac1d64d6b0b0f23cfaeda6843d766126166365 +Subproject commit 2c333561bf351b74b140194f201c56218a3cb3c3 diff --git a/tools/generate-eslint-meta.ts b/tools/generate-eslint-meta.ts new file mode 100644 index 00000000000..23fd3b62f6e --- /dev/null +++ b/tools/generate-eslint-meta.ts @@ -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, + }, + ); +} diff --git a/tools/generate-java-rule-classes.ts b/tools/generate-java-rule-classes.ts new file mode 100644 index 00000000000..1f217bd3034 --- /dev/null +++ b/tools/generate-java-rule-classes.ts @@ -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 = 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) { + 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 configurations() {\n return List.of(${configurations.join(',')});\n}\n`, + ); + } + return result; +} diff --git a/tools/generate-meta.ts b/tools/generate-meta.ts index 438dd638270..cca318fbfea 100644 --- a/tools/generate-meta.ts +++ b/tools/generate-meta.ts @@ -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(); diff --git a/tools/generate-rule-indexes.ts b/tools/generate-rule-indexes.ts index 5f9dfdba09b..5f329066abe 100644 --- a/tools/generate-rule-indexes.ts +++ b/tools/generate-rule-indexes.ts @@ -15,18 +15,39 @@ * along with this program; if not, see https://sonarsource.com/license/ssal/ */ import { - getAllJavaChecks, + DIRNAME, getAllRulesMetadata, - javaChecksPath, - RULES_FOLDER, header, inflateTemplateToFile, JAVA_TEMPLATES_FOLDER, + ruleRegex, + RULES_FOLDER, + sonarKeySorter, TS_TEMPLATES_FOLDER, } from './helpers.js'; import { join } from 'node:path'; +import { readdir } from 'fs/promises'; -await updateIndexes(); +/** + * Get path to Java source + * + * @param target whether get source path to "main" or "test" files + */ +function javaChecksPath(target: 'main' | 'test') { + return join( + DIRNAME, + '../', + 'sonar-plugin', + 'javascript-checks', + 'src', + target, + 'java', + 'org', + 'sonar', + 'javascript', + 'checks', + ); +} /** * Updates the following rules indexes, which are autogenerated and @@ -97,3 +118,14 @@ export async function updateIndexes() { }, ); } + +/** + * List all Java checks classes + */ +async function getAllJavaChecks() { + const files = await readdir(javaChecksPath('main'), { withFileTypes: true }); + return files + .filter(file => ruleRegex.test(file.name) && !file.isDirectory()) + .map(file => file.name.slice(0, -5)) // remove .java extension + .sort(sonarKeySorter); +} diff --git a/tools/helpers.ts b/tools/helpers.ts index 06ef0bde75e..6f5df26d6ec 100644 --- a/tools/helpers.ts +++ b/tools/helpers.ts @@ -15,37 +15,19 @@ * along with this program; if not, see https://sonarsource.com/license/ssal/ */ import prettier from 'prettier'; -import { readdir, writeFile, readFile, stat } from 'fs/promises'; -import { join, dirname } from 'node:path'; +import { readdir, readFile, stat, writeFile } from 'fs/promises'; +import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; //@ts-ignore import { prettier as prettierOpts } from '../package.json'; -import { - defaultOptions, - ESLintConfiguration, - ESLintConfigurationProperty, - ESLintConfigurationSQProperty, -} from '../packages/jsts/src/rules/helpers/configs.js'; -import assert from 'node:assert'; +import { ESLintConfiguration } from '../packages/jsts/src/rules/helpers/configs.js'; -const ruleRegex = /^S\d+/; +export const ruleRegex = /^S\d+/; export const DIRNAME = dirname(fileURLToPath(import.meta.url)); -const REPOSITORY_ROOT = join(DIRNAME, '..'); +export const REPOSITORY_ROOT = join(DIRNAME, '..'); export const TS_TEMPLATES_FOLDER = join(DIRNAME, 'templates', 'ts'); export const JAVA_TEMPLATES_FOLDER = join(DIRNAME, 'templates', 'java'); export const RULES_FOLDER = join(REPOSITORY_ROOT, 'packages', 'jsts', 'src', 'rules'); -const JAVA_CHECKS_FOLDER = join( - REPOSITORY_ROOT, - 'sonar-plugin', - 'javascript-checks', - 'src', - 'main', - 'java', - 'org', - 'sonar', - 'javascript', - 'checks', -); export const METADATA_FOLDER = join( REPOSITORY_ROOT, 'sonar-plugin', @@ -62,7 +44,7 @@ export const METADATA_FOLDER = join( ); export const header = await readFile(join(DIRNAME, 'header.ts'), 'utf8'); -const typeMatrix = { +export const typeMatrix = { CODE_SMELL: 'suggestion', BUG: 'problem', SECURITY_HOTSPOT: 'problem', @@ -79,13 +61,9 @@ type rspecMeta = { compatibleLanguages: ('JAVASCRIPT' | 'TYPESCRIPT')[]; }; -const sonarWayProfile = JSON.parse( - await readFile(join(METADATA_FOLDER, `Sonar_way_profile.json`), 'utf-8'), -); - // Array sorter for Sonar rule IDs const getInt = (sonarKey: string) => parseInt(/^S(\d+)/.exec(sonarKey)[1]); -const sonarKeySorter = (a: string, b: string) => getInt(a) - getInt(b); +export const sonarKeySorter = (a: string, b: string) => getInt(a) - getInt(b); export function verifyRuleName(eslintId: string) { const re = /^[a-z]+(-[a-z0-9]+)*$/; @@ -94,19 +72,12 @@ export function verifyRuleName(eslintId: string) { } } -export function verifyRspecId(sonarKey: string) { - const re = /^S\d+$/; - if (!re.exec(sonarKey)) { - throw new Error(`Invalid rspec key: it should match ${re}, but got "${sonarKey}"`); - } -} - /** * Inflate string template with given dictionary * @param text template string * @param dictionary object with the keys to replace */ -export function inflateTemplate(text: string, dictionary: { [x: string]: string }): string { +function inflateTemplate(text: string, dictionary: { [x: string]: string }): string { for (const key in dictionary) { text = text.replaceAll(key, dictionary[key]); } @@ -130,205 +101,9 @@ export async function inflateTemplateToFile( await writePrettyFile(dest, inflateTemplate(template, dict)); } -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( +export async function getESLintDefaultConfiguration( sonarKey: string, - defaults: { compatibleLanguages?: ('JAVASCRIPT' | 'TYPESCRIPT')[]; scope?: 'Main' | 'Tests' }, -) { - if (sonarKey === 'S1541') { - await inflate1541(); - return; - } - const ruleRspecMeta = await getRspecMeta(sonarKey, defaults); - const imports: Set = 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) { - 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 configurations() {\n return List.of(${configurations.join(',')});\n}\n`, - ); - } - return result; -} - -async function getESLintDefaultConfiguration(sonarKey: string): Promise { +): Promise { const configFilePath = join(RULES_FOLDER, sonarKey, 'config.ts'); const configFileExists = await exists(configFilePath); if (!configFileExists) { @@ -338,7 +113,7 @@ async function getESLintDefaultConfiguration(sonarKey: string): Promise { @@ -359,75 +134,6 @@ async function getRspecMeta( }; } -/** - * 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, - }, - ); -} - -/** - * Get path to Java source - * - * @param target whether get source path to "main" or "test" files - */ -export function javaChecksPath(target: 'main' | 'test') { - return join( - DIRNAME, - '../', - 'sonar-plugin', - 'javascript-checks', - 'src', - target, - 'java', - 'org', - 'sonar', - 'javascript', - 'checks', - ); -} - -/** - * List all Java checks classes - */ -export async function getAllJavaChecks() { - const files = await readdir(javaChecksPath('main'), { withFileTypes: true }); - return files - .filter(file => ruleRegex.test(file.name) && !file.isDirectory()) - .map(file => file.name.slice(0, -5)) // remove .java extension - .sort(sonarKeySorter); -} - /** * Get the metadata for all rules in SonarJS */ diff --git a/tools/new-rule.mts b/tools/new-rule.mts index 22c0a461220..7ee4a63dbc4 100644 --- a/tools/new-rule.mts +++ b/tools/new-rule.mts @@ -14,19 +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 { writeFile, readFile, mkdir } from 'node:fs/promises'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { checkbox, input, select } from '@inquirer/prompts'; import { DIRNAME, - generateJavaCheckClass, - generateMetaForRule, inflateTemplateToFile, + ruleRegex, RULES_FOLDER, TS_TEMPLATES_FOLDER, - verifyRspecId, verifyRuleName, } from './helpers.js'; +import { generateMetaForRule } from './generate-eslint-meta.js'; +import { generateJavaCheckClass } from './generate-java-rule-classes.js'; const header = await readFile(join(DIRNAME, 'header.ts'), 'utf8'); @@ -88,6 +88,12 @@ const hasSecondaries = await select({ ], }); +function verifyRspecId(sonarKey: string) { + if (!ruleRegex.exec(sonarKey)) { + throw new Error(`Invalid rspec key: it should match ${ruleRegex}, but got "${sonarKey}"`); + } +} + verifyRspecId(sonarKey); verifyRuleName(eslintId);