Skip to content

Commit

Permalink
JS-580 Update new rule script (#5178)
Browse files Browse the repository at this point in the history
  • Loading branch information
zglicz authored Mar 6, 2025
1 parent ab64477 commit 49de91b
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 53 deletions.
27 changes: 19 additions & 8 deletions tools/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,15 @@ async function inflate1541() {
);
}

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

if (ruleRspecMeta.compatibleLanguages.includes('JAVASCRIPT')) {
const derivedLanguages = ruleRspecMeta.compatibleLanguages;
if (derivedLanguages.includes('JAVASCRIPT')) {
decorators.push('@JavaScriptRule');
imports.add('import org.sonar.plugins.javascript.api.JavaScriptRule;');
}
if (ruleRspecMeta.compatibleLanguages.includes('TYPESCRIPT')) {
if (derivedLanguages.includes('TYPESCRIPT')) {
decorators.push('@TypeScriptRule');
imports.add('import org.sonar.plugins.javascript.api.TypeScriptRule;');
}
Expand Down Expand Up @@ -328,7 +332,10 @@ async function getESLintDefaultConfiguration(sonarKey: string): Promise<ESLintCo
return config.fields;
}

async function getRspecMeta(sonarKey: string): Promise<rspecMeta> {
async function getRspecMeta(
sonarKey: string,
defaults: { compatibleLanguages?: ('JAVASCRIPT' | 'TYPESCRIPT')[]; scope?: 'Main' | 'Tests' },
): Promise<rspecMeta> {
const rspecFile = join(METADATA_FOLDER, `${sonarKey}.json`);
const rspecFileExists = await exists(rspecFile);
if (!rspecFileExists) {
Expand All @@ -342,17 +349,21 @@ async function getRspecMeta(sonarKey: string): Promise<rspecMeta> {
tags: [],
type: 'BUG',
status: 'ready',
quickfix: 'covered',
...defaults,
};
}

/**
* 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) {
const ruleRspecMeta = await getRspecMeta(sonarKey);
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}`);
}
Expand Down
78 changes: 45 additions & 33 deletions tools/new-rule.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
*/
import { writeFile, readFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { input, select } from '@inquirer/prompts';
import { checkbox, input, select } from '@inquirer/prompts';
import {
DIRNAME,
generateJavaCheckClass,
generateMetaForRule,
inflateTemplateToFile,
JAVA_TEMPLATES_FOLDER,
javaChecksPath,
RULES_FOLDER,
TS_TEMPLATES_FOLDER,
verifyRspecId,
Expand All @@ -33,17 +32,17 @@ const header = await readFile(join(DIRNAME, 'header.ts'), 'utf8');

const sonarKey = await input({ message: 'Enter the Sonar key for the new rule (SXXXX)' });
const eslintId = await input({ message: 'Enter the ESLint ID for the rule' });
const ruleTarget = await select({
const scope = (await select({
message: 'What code does the rule target?',
choices: [
{
value: 'MAIN',
value: 'Main',
},
{
value: 'TEST',
value: 'Tests',
},
],
});
})) satisfies 'Main' | 'Tests';
const implementation = await select({
message: 'Origin of the rule',
choices: [
Expand All @@ -61,6 +60,33 @@ const implementation = await select({
},
],
});
const languages = (await checkbox({
message: 'What languages will the rule support?',
choices: [
{
value: 'JAVASCRIPT',
checked: true,
},
{
value: 'TYPESCRIPT',
checked: true,
},
],
required: true,
})) satisfies ('JAVASCRIPT' | 'TYPESCRIPT')[];
const hasSecondaries = await select({
message: 'Will the rule produce secondary locations?',
choices: [
{
value: true,
name: 'Yes',
},
{
value: false,
name: 'No',
},
],
});

verifyRspecId(sonarKey);
verifyRuleName(eslintId);
Expand All @@ -71,7 +97,7 @@ await mkdir(ruleFolder, { recursive: true });

if (implementation !== 'external') {
// index.ts
await writeFile(join(ruleFolder, `index.ts`), `${header}export { rule } from './rule';\n`);
await writeFile(join(ruleFolder, `index.ts`), `${header}export { rule } from './rule.js';\n`);
// rule.ts
await inflateTemplateToFile(
join(
Expand Down Expand Up @@ -108,10 +134,14 @@ if (implementation !== 'external') {
// meta.ts
let extra = '';
if (implementation === 'decorated') {
extra = `export const externalRules = [\n { externalPlugin: 'plugin-name', externalRule: '${eslintId}' },\n];`;
extra = `export const externalRules = [\n { externalPlugin: 'plugin-name', externalRule: '${eslintId}' },\n];\n`;
} else if (implementation === 'external') {
extra = `export const externalPlugin = 'plugin-name';`;
extra = `export const externalPlugin = 'plugin-name';\n`;
}
if (hasSecondaries) {
extra += `export const hasSecondaries = true;\n`;
}
console.log(JSON.stringify(languages));
await inflateTemplateToFile(
join(TS_TEMPLATES_FOLDER, 'meta.template'),
join(ruleFolder, `meta.ts`),
Expand All @@ -125,32 +155,14 @@ await inflateTemplateToFile(
);

// preliminary generated-meta.ts
await generateMetaForRule(sonarKey);

// Create rule java source from template
await inflateTemplateToFile(
join(JAVA_TEMPLATES_FOLDER, ruleTarget === 'MAIN' ? 'rule.main.template' : 'rule.test.template'),
join(javaChecksPath('main'), `${sonarKey}.java`),
{
___RULE_KEY___: sonarKey,
___PROPERTIES___: await readFile(join(JAVA_TEMPLATES_FOLDER, 'properties'), 'utf8'),
___HEADER___: header,
},
);
await generateMetaForRule(sonarKey, { compatibleLanguages: languages, scope });

// Create rule java test from template
await inflateTemplateToFile(
join(JAVA_TEMPLATES_FOLDER, 'ruletest.template'),
join(javaChecksPath('test'), `${sonarKey}Test.java`),
{
___RULE_KEY___: sonarKey,
___HEADER___: header,
},
);
// generate rule java class
await generateJavaCheckClass(sonarKey, { compatibleLanguages: languages, scope });

console.log(`
STEPS
1. If your rule accepts parameters, please add the JSON Schema to "sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/schemas"
NEXT STEPS:
1. If your rule accepts parameters, you can customize them in a 'config.ts' file
2. After RSPEC for the new rule has been generated, run 'npm run generate-meta'
`);

Expand Down
3 changes: 2 additions & 1 deletion tools/templates/ts/rule.cbtest.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { check } from '../../../tests/tools/testers/comment-based/index.js';
import { rule } from './index.js';
import path from 'path';
import { describe } from 'node:test';
import * as meta from './meta.js';

const sonarId = path.basename(import.meta.dirname);

describe(`Rule ${sonarId}`, () => {
check(sonarId, rule, import.meta.dirname);
check(meta, rule, import.meta.dirname);
});
4 changes: 2 additions & 2 deletions tools/templates/ts/rule.decorated.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
interceptReport,
mergeRules,
} from '../helpers/index.js';
import { meta } from './meta.js';
import * as meta from './meta.js';

// you can return the decoratedRule
export const rule: Rule.RuleModule = interceptReport(
Expand All @@ -23,7 +23,7 @@ export const rule: Rule.RuleModule = interceptReport(

// or return the merger of two or more rules together
export const rule: Rule.RuleModule = {
meta: generateMeta(meta as Rule.RuleMetaData, {
meta: generateMeta(meta, {
hasSuggestions: true,
messages: {
...rule1.meta?.messages,
Expand Down
16 changes: 7 additions & 9 deletions tools/templates/ts/rule.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import {
toSecondaryLocation,
} from '../helpers/index.js';
import estree from 'estree';
import { FromSchema } from 'json-schema-to-ts';
// if rule has schema, provide it in
// sonar-plugin/javascript-checks/src/main/resources/org/sonar/l10n/javascript/rules/javascript/schemas/
// and re-run npm run generate-meta, otherwise remove the schema import
import { meta, schema } from './meta.js';
// If a rule has a schema, use this to extract it.
// import { FromSchema } from 'json-schema-to-ts';
import * as meta from './meta.js';

const messages = {
//TODO: add needed messages
Expand All @@ -22,24 +20,24 @@ const messages = {
const DEFAULT_PARAM = 10;

export const rule: Rule.RuleModule = {
meta: generateMeta(meta as Rule.RuleMetaData, { schema, messages }, true /* false if no secondary locations */ ),
meta: generateMeta(meta, { messages }),
create(context: Rule.RuleContext) {
// remove this condition if the rule does not depend on TS type-checker
const services = context.sourceCode.parserServices;
if (!isRequiredParserServices(services)) {
return {};
}

// get typed rule options with FromSchema helper
const param = (context.options as FromSchema<typeof schema>)[0]?.param ?? DEFAULT_PARAM;
const services = context.parserServices;
// const param = (context.options as FromSchema<typeof meta.schema>)[0]?.param ?? DEFAULT_PARAM;

return {
//example
Identifier(node: estree.Identifier) {
const secondaries: estree.Node[] = [];
const message = 'message body';
const messageId = 'messageId'; // must exist in messages object of rule metadata
if (param) {
if (DEFAULT_PARAM) {
// Use context.report if rule does not use secondary locations
report(
context,
Expand Down

0 comments on commit 49de91b

Please sign in to comment.