Skip to content

Commit

Permalink
CHANGE(pmd): @W-17386449@: Small refactor for how we files to languages
Browse files Browse the repository at this point in the history
  • Loading branch information
stephen-carter-at-sf committed Dec 11, 2024
1 parent d09115e commit 484b5a3
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ List<PmdRuleInfo> describeRulesFor(List<String> customRulesets, Set<String> lang
// Add rule info
PmdRuleInfo pmdRuleInfo = new PmdRuleInfo();
pmdRuleInfo.name = rule.getName();
pmdRuleInfo.language = language;
pmdRuleInfo.languageId = language;
pmdRuleInfo.description = getLimitedDescription(rule);
pmdRuleInfo.externalInfoUrl = rule.getExternalInfoUrl();
pmdRuleInfo.ruleSet = rule.getRuleSetName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class PmdRuleInfo {
public String name;
public String language;
public String languageId;
public String description;
public String externalInfoUrl;
public String ruleSet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void whenDescribeRulesForApex_thenCorrectRulesAreReturned() {
List<PmdRuleInfo> ruleInfoList = ruleDescriber.describeRulesFor(List.of(), Set.of("apex"));
assertThat(ruleInfoList.size(), is(greaterThan(0))); // Leaving this flexible. The actual list of rules are tested by typescript tests.
for (PmdRuleInfo ruleInfo : ruleInfoList) {
assertThat(ruleInfo.language, is("apex"));
assertThat(ruleInfo.languageId, is("apex"));
}

// Sanity check one of the rules:
Expand All @@ -76,7 +76,7 @@ void whenDescribeRulesForEcmascript_thenCorrectRulesAreReturned() {
List<PmdRuleInfo> ruleInfoList = ruleDescriber.describeRulesFor(List.of(), Set.of("ecmascript"));
assertThat(ruleInfoList.size(), is(greaterThan(0))); // Leaving this flexible. The actual list of rules are tested by typescript tests.
for (PmdRuleInfo ruleInfo : ruleInfoList) {
assertThat(ruleInfo.language, is("ecmascript"));
assertThat(ruleInfo.languageId, is("ecmascript"));
}

// Sanity check one of the rules:
Expand All @@ -93,7 +93,7 @@ void whenDescribeRulesForVisualforce_thenCorrectRulesAreReturned() {
List<PmdRuleInfo> ruleInfoList = ruleDescriber.describeRulesFor(List.of(), Set.of("visualforce"));
assertThat(ruleInfoList.size(), is(greaterThan(0))); // Leaving this flexible. The actual list of rules are tested by typescript tests.
for (PmdRuleInfo ruleInfo : ruleInfoList) {
assertThat(ruleInfo.language, is("visualforce"));
assertThat(ruleInfo.languageId, is("visualforce"));
}

// Sanity check one of the rules:
Expand All @@ -110,7 +110,7 @@ void whenDescribeRulesForXml_thenCorrectRulesAreReturned() {
List<PmdRuleInfo> ruleInfoList = ruleDescriber.describeRulesFor(List.of(), Set.of("xml"));
assertThat(ruleInfoList.size(), is(greaterThan(0))); // Leaving this flexible. The actual list of rules are tested by typescript tests.
for (PmdRuleInfo ruleInfo : ruleInfoList) {
assertThat(ruleInfo.language, is("xml"));
assertThat(ruleInfo.languageId, is("xml"));
}

// Sanity check one of the rules:
Expand Down Expand Up @@ -246,7 +246,7 @@ static String createSampleRuleset(String rulesetName, String ruleName, String la
static PmdRuleInfo assertContainsOneRuleWithNameAndLanguage(List<PmdRuleInfo> ruleInfoList, String ruleName, String language) {
PmdRuleInfo ruleFound = null;
for (PmdRuleInfo ruleInfo : ruleInfoList) {
if (ruleInfo.name.equals(ruleName) && ruleInfo.language.equals(language)) {
if (ruleInfo.name.equals(ruleName) && ruleInfo.languageId.equals(language)) {
if(ruleFound != null) {
throw new RuntimeException("The ruleInfoList contained more than one rule with name \"" + ruleName + "\" and language \"" + language + "\"");
}
Expand All @@ -261,7 +261,7 @@ static PmdRuleInfo assertContainsOneRuleWithNameAndLanguage(List<PmdRuleInfo> ru

static void assertContainsNoRuleWithNameAndLanguage(List<PmdRuleInfo> ruleInfoList, String ruleName, String language) {
for (PmdRuleInfo ruleInfo : ruleInfoList) {
if (ruleInfo.name.equals(ruleName) && ruleInfo.language.equals(language)) {
if (ruleInfo.name.equals(ruleName) && ruleInfo.languageId.equals(language)) {
throw new RuntimeException("The ruleInfoList unexpectedly contained a rule with name \"" + ruleName + "\" and language \"" + language + "\"");
}
}
Expand Down
42 changes: 21 additions & 21 deletions packages/code-analyzer-pmd-engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {JavaVersionIdentifier} from "./JavaVersionIdentifier";
import {SemVer} from "semver";
import path from "node:path";
import {indent} from "./utils";
import {MINIMUM_JAVA_VERSION, LanguageId, CPD_ENGINE_NAME, PMD_ENGINE_NAME} from "./constants";
import {MINIMUM_JAVA_VERSION, Language, CPD_ENGINE_NAME, PMD_ENGINE_NAME} from "./constants";
import fs from "node:fs";

export const PMD_AVAILABLE_LANGUAGES: string[] = Object.values(LanguageId).filter(l => l !== LanguageId.TYPESCRIPT); // Typescript is available in CPD but not PMD
export const CPD_AVAILABLE_LANGUAGES: string[] = Object.values(LanguageId);
export const PMD_AVAILABLE_LANGUAGES: Language[] = Object.values(Language).filter(l => l !== Language.TYPESCRIPT); // Typescript is available in CPD but not PMD
export const CPD_AVAILABLE_LANGUAGES: Language[] = Object.values(Language);

const DEFAULT_JAVA_COMMAND: string = 'java';

Expand All @@ -23,7 +23,7 @@ export type PmdEngineConfig = {
// List of languages associated with the PMD rules to be made available for 'pmd' engine rule selection.
// The languages that you may choose from are: 'apex', 'html', 'javascript' (or 'ecmascript'), 'visualforce', 'xml'
// See https://pmd.github.io/pmd/tag_rule_references.html to learn about the PMD rules available for each language.
rule_languages: string[]
rule_languages: Language[]

// List of jar files and/or folders to add the Java classpath when running PMD.
// Each entry may be given as an absolute path or a relative path to 'config_root'.
Expand Down Expand Up @@ -84,15 +84,15 @@ export type CpdEngineConfig = {
// !! NOTE !! - HIDDEN UNTIL A USER REQUESTS THIS - ALL LANGUAGES ARE ENABLED BY DEFAULT SO THERE MAY NOT BE A USE CASE FOR THIS YET
// List of languages associated with CPD to be made available for 'cpd' engine rule selection.
// The languages that you may choose from are: 'apex', 'html', 'javascript' (or 'ecmascript'), 'typescript', 'visualforce', 'xml'
rule_languages: string[]
rule_languages: Language[]

// Specifies the minimum tokens threshold for each rule language.
// The minimum tokens threshold is the number of tokens required to be in a duplicate block of code in order to be
// reported as a violation. The concept of a token may be defined differently per language, but in general it is a
// distinct basic element of source code. For example, this could be language specific keywords, identifiers,
// operators, literals, and more. See https://docs.pmd-code.org/latest/pmd_userdocs_cpd.html to learn more.
// If a value for a language is unspecified, then the default value of 100 will be used for that language.
minimum_tokens: Record<string, number>
minimum_tokens: Record<Language, number>

// Indicates whether to ignore multiple copies of files of the same name and length.
skip_duplicate_files: boolean
Expand All @@ -103,10 +103,10 @@ const DEFAULT_MINIMUM_TOKENS: number = 100;
export const DEFAULT_CPD_ENGINE_CONFIG: CpdEngineConfig = {
java_command: DEFAULT_JAVA_COMMAND,
rule_languages: CPD_AVAILABLE_LANGUAGES, // hidden
minimum_tokens: CPD_AVAILABLE_LANGUAGES.reduce((obj, lang: string) => {
minimum_tokens: CPD_AVAILABLE_LANGUAGES.reduce((obj, lang: Language) => {
obj[lang] = DEFAULT_MINIMUM_TOKENS;
return obj;
}, {} as Record<string, number>),
}, {} as Record<Language, number>),
skip_duplicate_files: false
}

Expand Down Expand Up @@ -174,10 +174,10 @@ abstract class SharedConfigValueExtractor {
}

// The list of all possible languages that can be chosen within the rule_languages array
protected abstract getAllPossibleRuleLanguages(): string[];
protected abstract getAllPossibleRuleLanguages(): Language[];

// The default return value for rule_languages
protected abstract getDefaultRuleLanguages(): string[];
protected abstract getDefaultRuleLanguages(): Language[];

async extractJavaCommand(): Promise<string> {
const javaCommand: string | undefined = this.configValueExtractor.extractString('java_command');
Expand Down Expand Up @@ -239,7 +239,7 @@ abstract class SharedConfigValueExtractor {
}
}

extractRuleLanguages(): string[] {
extractRuleLanguages(): Language[] {
let ruleLanguages: string[] | undefined = this.configValueExtractor.extractArray('rule_languages',
ValueValidator.validateString);
if (ruleLanguages === undefined) {
Expand All @@ -258,14 +258,14 @@ abstract class SharedConfigValueExtractor {
// Validate each language is supported
ruleLanguages = [...ruleLanguagesSet];
for (const ruleLanguage of ruleLanguages) {
if (!this.getAllPossibleRuleLanguages().includes(ruleLanguage)) {
if (!(this.getAllPossibleRuleLanguages() as string[]).includes(ruleLanguage)) {
throw new Error(getMessage('InvalidRuleLanguage',
this.configValueExtractor.getFieldPath('rule_languages'),
ruleLanguage,
toAvailableLanguagesText(this.getAllPossibleRuleLanguages())));
}
}
return ruleLanguages.sort();
return ruleLanguages.sort() as Language[];
}
}

Expand Down Expand Up @@ -313,11 +313,11 @@ class PmdConfigValueExtractor extends SharedConfigValueExtractor {
return [... new Set(customRulesets)]; // Converting to set and back to array to remove duplicates
}

protected getAllPossibleRuleLanguages(): string[] {
protected getAllPossibleRuleLanguages(): Language[] {
return PMD_AVAILABLE_LANGUAGES;
}

protected getDefaultRuleLanguages(): string[] {
protected getDefaultRuleLanguages(): Language[] {
return DEFAULT_PMD_ENGINE_CONFIG.rule_languages;
}
}
Expand All @@ -327,20 +327,20 @@ class CpdConfigValueExtractor extends SharedConfigValueExtractor {
super(configValueExtractor, javaVersionIdentifier);
}

protected getAllPossibleRuleLanguages(): string[] {
protected getAllPossibleRuleLanguages(): Language[] {
return CPD_AVAILABLE_LANGUAGES;
}

protected getDefaultRuleLanguages(): string[] {
protected getDefaultRuleLanguages(): Language[] {
return DEFAULT_CPD_ENGINE_CONFIG.rule_languages;
}

extractMinimumTokens(): Record<string, number> {
extractMinimumTokens(): Record<Language, number> {
const minimumTokensExtractor: ConfigValueExtractor = this.configValueExtractor.extractObjectAsExtractor(
'minimum_tokens', DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens);

// Start with a copy will all the default values
const minimumTokensMap: Record<string, number> = {...DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens};
const minimumTokensMap: Record<Language, number> = {...DEFAULT_CPD_ENGINE_CONFIG.minimum_tokens};

// And override the default values with user provided values for each language found
for (const key of minimumTokensExtractor.getKeys()) {
Expand All @@ -350,7 +350,7 @@ class CpdConfigValueExtractor extends SharedConfigValueExtractor {
language = 'javascript';
}

if (!CPD_AVAILABLE_LANGUAGES.includes(language)) {
if (!(CPD_AVAILABLE_LANGUAGES as string[]).includes(language)) {
throw new Error(getMessage('InvalidFieldKeyForObject',
this.configValueExtractor.getFieldPath('minimum_tokens'), key, toAvailableLanguagesText(CPD_AVAILABLE_LANGUAGES)));
}
Expand All @@ -359,7 +359,7 @@ class CpdConfigValueExtractor extends SharedConfigValueExtractor {
if (minimumTokensValue <= 0 || Math.floor(minimumTokensValue) != minimumTokensValue) {
throw new Error(getMessage('InvalidPositiveInteger', minimumTokensExtractor.getFieldPath(key)));
}
minimumTokensMap[language] = minimumTokensValue;
minimumTokensMap[language as Language] = minimumTokensValue;
}

return minimumTokensMap;
Expand Down
83 changes: 51 additions & 32 deletions packages/code-analyzer-pmd-engine/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ export const PMD_VERSION: string = '7.8.0';
export const PMD_ENGINE_NAME: string = "pmd";
export const CPD_ENGINE_NAME: string = "cpd";

// !!! IMPORTANT !!! KEEP THIS IN SYNC WITH gradle/libs.versions.toml
export enum LanguageId {
// The minimum required java version that users must have to run the 'pmd' engine
export const MINIMUM_JAVA_VERSION = '11.0.0';

// !!! IMPORTANT !!! KEEP THIS LIST OF LANGUAGES IN SYNC WITH gradle/libs.versions.toml
export enum Language {
APEX = 'apex', // [CPD+PMD]
HTML = 'html', // [CPD+PMD]
JAVASCRIPT = 'javascript', // [CPD+PMD] Note that this id gets converted to 'ecmascript' when communicating to PMD since it uses 'ecmascript' instead as the identifier
Expand All @@ -14,42 +17,58 @@ export enum LanguageId {
XML = 'xml' // [CPD+PMD]
}

// The minimum required java version that users must have to run the 'pmd' engine
export const MINIMUM_JAVA_VERSION = '11.0.0';
// Default map from language to file extensions to be associated with that language
// Even though we force the file extensions per language to give us full control over the file extensions, minimally
// we should support the default file associations found in the language model definitions:
// * Apex ('apex') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ApexLanguageModule.java
// * HTML ('html') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-html/src/main/java/net/sourceforge/pmd/lang/html/HtmlLanguageModule.java
// * JavaScript ('ecmascript') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-javascript/src/main/java/net/sourceforge/pmd/lang/ecmascript/EcmascriptLanguageModule.java
// * Typescript ('typescript') Language Module [CPD Only - No PMD Support]: https://github.com/pmd/pmd/blob/master/pmd-javascript/src/main/java/net/sourceforge/pmd/lang/typescript/TsLanguageModule.java
// * Visualforce ('visualforce') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/visualforce/VfLanguageModule.java
// * XML ('xml') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-xml/src/main/java/net/sourceforge/pmd/lang/xml/XmlLanguageModule.java
// Additionally, we must support the file extensions that the AppExchange rules want to also process per language.
export const DEFAULT_FILE_EXTENSIONS: Record<Language, string[]> = {
[Language.APEX]: [
// From PMD's ApexLanguageModule:
'.cls', '.trigger'
],

// Map from file extension to language for PMD or CPD.
// In the future we might consider getting this dynamically from our PmdWrapper (java side) instead of hard coding
// these, but hard coding actually makes things much faster so that we have one less round trip to java to perform.
export const extensionToLanguageId: Record<string, LanguageId> = {
// (Apex - 'apex') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/ApexLanguageModule.java
'.cls': LanguageId.APEX,
'.trigger': LanguageId.APEX,
[Language.HTML]: [
// From PMD's HtmlLanguageModule:
'.html', '.htm', '.xhtml', '.xht', '.shtml'
],

// (JavaScript - 'ecmascript') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-javascript/src/main/java/net/sourceforge/pmd/lang/ecmascript/EcmascriptLanguageModule.java
'.js': LanguageId.JAVASCRIPT,
[Language.JAVASCRIPT]: [
// From PMD's EcmascriptLanguageModule:
'.js',

// (HTML - 'html') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-html/src/main/java/net/sourceforge/pmd/lang/html/HtmlLanguageModule.java
'.html': LanguageId.HTML,
'.htm': LanguageId.HTML,
'.xhtml': LanguageId.HTML,
'.xht': LanguageId.HTML,
'.shtml': LanguageId.HTML,
// Other common JavasScript file extensions that we wish to include by default:
'.cjs', '.mjs'
],

// (Salesforce Visualforce - 'visualforce') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/visualforce/VfLanguageModule.java
'.page': LanguageId.VISUALFORCE,
'.component': LanguageId.VISUALFORCE,
[Language.TYPESCRIPT]: [
// From PMD's TsLanguageModule:
'.ts'
],

// (Typescript - 'typescript') Language Module [CPD Only - No PMD Support]: https://github.com/pmd/pmd/blob/master/pmd-javascript/src/main/java/net/sourceforge/pmd/lang/typescript/TsLanguageModule.java
'.ts': LanguageId.TYPESCRIPT,
[Language.VISUALFORCE]: [
// FROM PMD's VfLanguageModule:
'.page', '.component'
],

// (XML - 'xml') Language Model [CPD+PMD]: https://github.com/pmd/pmd/blob/master/pmd-xml/src/main/java/net/sourceforge/pmd/lang/xml/XmlLanguageModule.java
'.xml': LanguageId.XML
} as const;
[Language.XML]: [
// FROM PMD's XmlLanguageModule:
'.xml'

// Salesforce metadata file extensions to associate to XML language, specifically for the AppExchange rules:
// TODO: COMING SOON
]
}

// This object lists all the PMD rule names that are shared across languages which helps us map back and forth to unique names
export const SHARED_RULE_NAMES: Record<string, LanguageId[]> = {
ForLoopsMustUseBraces: [LanguageId.APEX, LanguageId.JAVASCRIPT],
IfElseStmtsMustUseBraces: [LanguageId.APEX, LanguageId.JAVASCRIPT],
IfStmtsMustUseBraces: [LanguageId.APEX, LanguageId.JAVASCRIPT],
WhileLoopsMustUseBraces: [LanguageId.APEX, LanguageId.JAVASCRIPT]
export const SHARED_RULE_NAMES: Record<string, Language[]> = {
ForLoopsMustUseBraces: [Language.APEX, Language.JAVASCRIPT],
IfElseStmtsMustUseBraces: [Language.APEX, Language.JAVASCRIPT],
IfStmtsMustUseBraces: [Language.APEX, Language.JAVASCRIPT],
WhileLoopsMustUseBraces: [Language.APEX, Language.JAVASCRIPT]
};
Loading

0 comments on commit 484b5a3

Please sign in to comment.