Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 29 additions & 12 deletions packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,16 @@ export class CodeAnalyzer {
return;
}

const engineOverrides: EngineOverrides = this.config.getEngineOverridesFor(engineName);

try {
const engineConfigDescription: engApi.ConfigDescription = enginePluginV1.describeEngineConfig(engineName);
normalizeEngineConfigDescription(engineConfigDescription, engineName);
this.engineConfigDescriptions.set(engineName, engineConfigDescription);
const configDescription: ConfigDescription = toConfigDescription(engineConfigDescription, engineName, engineOverrides);
this.engineConfigDescriptions.set(engineName, configDescription);
} catch (err) {
throw new Error(getMessage('PluginErrorWhenCreatingEngine', engineName, (err as Error).message));
}

const engineOverrides: EngineOverrides = this.config.getEngineOverridesFor(engineName);
if (engineOverrides[FIELDS.DISABLE_ENGINE]) {
this.emitLogEvent(LogLevel.Debug, getMessage('EngineDisabled', engineName,
`${FIELDS.ENGINES}.${engineName}.${FIELDS.DISABLE_ENGINE}`))
Expand Down Expand Up @@ -524,15 +525,31 @@ function isIntegerBetween(value: number, leftBound: number, rightBound: number):
return value >= leftBound && value <= rightBound && Number.isInteger(value);
}

function normalizeEngineConfigDescription(engineConfigDescription: ConfigDescription, engineName: string): void {
// Every engine config should have an overview, so if missing, then we add in a generic one
if (!engineConfigDescription.overview) {
engineConfigDescription.overview = getMessage('GenericEngineConfigOverview', engineName.toUpperCase());
/**
* Converts an engApi.ConfigDescription into a normalized ConfigDescription
*/
function toConfigDescription(engineConfigDescription: engApi.ConfigDescription, engineName: string,
engineOverrides: EngineOverrides): ConfigDescription {
const configDescription: ConfigDescription = {
// Every engine config should have an overview, so if missing, then we add in a generic one
overview: engineConfigDescription.overview ? engineConfigDescription.overview :
getMessage('GenericEngineConfigOverview', engineName.toUpperCase()),

fieldDescriptions: {
// Every engine config should have a disable_engine field which we prefer to be first in the object for display purposes
[FIELDS.DISABLE_ENGINE]: {
descriptionText: getMessage('EngineConfigFieldDescription_disable_engine', engineName),
valueType: "boolean",
defaultValue: false,
wasSuppliedByUser: FIELDS.DISABLE_ENGINE in engineOverrides
}
}
}

// Every engine config should have a disable_engine field which we prefer to be first in the object for display purposes
engineConfigDescription.fieldDescriptions = {
[FIELDS.DISABLE_ENGINE]: getMessage('EngineConfigFieldDescription_disable_engine', engineName),
... engineConfigDescription.fieldDescriptions
for (const fieldName in engineConfigDescription.fieldDescriptions) {
configDescription.fieldDescriptions[fieldName] = {
... engineConfigDescription.fieldDescriptions[fieldName],
wasSuppliedByUser: fieldName in engineOverrides
};
}
return configDescription;
}
69 changes: 51 additions & 18 deletions packages/code-analyzer-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as yaml from 'js-yaml';
import {getMessage} from "./messages";
import {toAbsolutePath} from "./utils"
import {SeverityLevel} from "./rules";
import {getMessageFromCatalog, SHARED_MESSAGE_CATALOG, ValueValidator} from "@salesforce/code-analyzer-engine-api";

export const FIELDS = {
CONFIG_ROOT: 'config_root',
Expand All @@ -27,15 +26,6 @@ export type RuleOverride = {
severity?: SeverityLevel
tags?: string[]
}
export const TOP_LEVEL_CONFIG_DESCRIPTION: ConfigDescription = {
overview: getMessage('ConfigOverview'),
fieldDescriptions: {
config_root: getMessage('ConfigFieldDescription_config_root'),
log_folder: getMessage('ConfigFieldDescription_log_folder'),
rules: getMessage('ConfigFieldDescription_rules'),
engines: getMessage('ConfigFieldDescription_engines'),
}
}

type TopLevelConfig = {
config_root: string
Expand All @@ -53,7 +43,20 @@ export const DEFAULT_CONFIG: TopLevelConfig = {
custom_engine_plugin_modules: [], // INTERNAL USE ONLY
};

export type ConfigDescription = engApi.ConfigDescription;
export type ConfigDescription = {
// A brief overview of this specific configuration object. It is recommended to include a link to documentation when possible.
overview: string

// Description objects for the primary fields in the configuration
fieldDescriptions: Record<string, ConfigFieldDescription>
}

export type ConfigFieldDescription = engApi.ConfigFieldDescription & {
// Whether or not the user has supplied a value for the field in their configuration file
// Note: Unlike the Engine API's ConfigFieldDescription, core has the ability to determine if the user supplied
// the field value (as we create a CodeAnalyzerConfig object), which is why "wasSuppliedByUser" is here only.
wasSuppliedByUser: boolean
}

export class CodeAnalyzerConfig {
private readonly config: TopLevelConfig;
Expand Down Expand Up @@ -99,16 +102,44 @@ export class CodeAnalyzerConfig {
config_root: configRoot,
log_folder: configExtractor.extractFolder(FIELDS.LOG_FOLDER, DEFAULT_CONFIG.log_folder)!,
custom_engine_plugin_modules: configExtractor.extractArray(FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES,
ValueValidator.validateString,
engApi.ValueValidator.validateString,
DEFAULT_CONFIG.custom_engine_plugin_modules)!,
rules: extractRulesValue(configExtractor),
engines: extractEnginesValue(configExtractor)
}
return new CodeAnalyzerConfig(config);
}

public static getConfigDescription(): ConfigDescription {
return TOP_LEVEL_CONFIG_DESCRIPTION;
public getConfigDescription(): ConfigDescription {
return {
overview: getMessage('ConfigOverview'),
fieldDescriptions: {
config_root: {
descriptionText: getMessage('ConfigFieldDescription_config_root'),
valueType: 'string',
defaultValue: null, // Using null for doc and since it indicates that the value is calculated based on the environment
wasSuppliedByUser: this.config.config_root !== DEFAULT_CONFIG.config_root
},
log_folder: {
descriptionText: getMessage('ConfigFieldDescription_log_folder'),
valueType: 'string',
defaultValue: null, // Using null for doc and since it indicates that the value is calculated based on the environment
wasSuppliedByUser: this.config.log_folder !== DEFAULT_CONFIG.log_folder
},
rules: {
descriptionText: getMessage('ConfigFieldDescription_rules'),
valueType: 'object',
defaultValue: {},
wasSuppliedByUser: this.config.rules !== DEFAULT_CONFIG.rules
},
engines: {
descriptionText: getMessage('ConfigFieldDescription_engines'),
valueType: 'object',
defaultValue: {},
wasSuppliedByUser: this.config.engines !== DEFAULT_CONFIG.engines
}
}
};
}

private constructor(config: TopLevelConfig) {
Expand Down Expand Up @@ -160,7 +191,7 @@ function extractRuleOverridesFrom(engineRuleOverridesExtractor: engApi.ConfigVal
function extractRuleOverrideFrom(ruleOverrideExtractor: engApi.ConfigValueExtractor): RuleOverride {
const engSeverity: engApi.SeverityLevel | undefined = ruleOverrideExtractor.extractSeverityLevel(FIELDS.SEVERITY);
return {
tags: ruleOverrideExtractor.extractArray(FIELDS.TAGS, ValueValidator.validateString),
tags: ruleOverrideExtractor.extractArray(FIELDS.TAGS, engApi.ValueValidator.validateString),
severity: engSeverity === undefined ? undefined : engSeverity as SeverityLevel
}
}
Expand Down Expand Up @@ -194,17 +225,19 @@ function parseAndValidate(parseFcn: () => unknown): object {
function validateAbsoluteFolder(value: unknown, fieldPath: string): string {
const folderValue: string = validateAbsolutePath(value, fieldPath);
if (!fs.statSync(folderValue).isDirectory()) {
throw new Error(getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigFolderValueMustNotBeFile', fieldPath, folderValue));
throw new Error(engApi.getMessageFromCatalog(engApi.SHARED_MESSAGE_CATALOG,
'ConfigFolderValueMustNotBeFile', fieldPath, folderValue));
}
return folderValue;
}

function validateAbsolutePath(value: unknown, fieldPath: string): string {
const pathValue: string = ValueValidator.validateString(value, fieldPath);
const pathValue: string = engApi.ValueValidator.validateString(value, fieldPath);
if (pathValue !== toAbsolutePath(pathValue)) {
throw new Error(getMessage('ConfigPathValueMustBeAbsolute', fieldPath, pathValue, toAbsolutePath(pathValue)));
} else if (!fs.existsSync(pathValue)) {
throw new Error(getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigPathValueDoesNotExist', fieldPath, pathValue));
throw new Error(engApi.getMessageFromCatalog(engApi.SHARED_MESSAGE_CATALOG,
'ConfigPathValueDoesNotExist', fieldPath, pathValue));
}
return pathValue;
}
1 change: 1 addition & 0 deletions packages/code-analyzer-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
CodeAnalyzerConfig,
ConfigDescription,
ConfigFieldDescription,
EngineOverrides,
RuleOverrides,
RuleOverride
Expand Down
37 changes: 31 additions & 6 deletions packages/code-analyzer-core/test/add-engines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,31 @@ describe("Tests for adding engines to Code Analyzer", () => {
expect(engineConfigDescription1).toEqual({
overview: "OverviewForStub1",
fieldDescriptions: {
disable_engine: getMessage('EngineConfigFieldDescription_disable_engine', 'stubEngine1'),
miscSetting1: "someDescriptionFor_miscSetting1"
disable_engine: {
descriptionText: getMessage('EngineConfigFieldDescription_disable_engine', 'stubEngine1'),
valueType: "boolean",
defaultValue: false,
wasSuppliedByUser: false
},
misc_value: {
descriptionText: "someDescriptionFor_misc_value",
valueType: "number",
defaultValue: 1987,
wasSuppliedByUser: false
}
}
});
const engineConfigDescription2: ConfigDescription = codeAnalyzer.getEngineConfigDescription('stubEngine2');
expect(engineConfigDescription2).toEqual({
overview: getMessage('GenericEngineConfigOverview', 'STUBENGINE2'),
fieldDescriptions: {
disable_engine: getMessage('EngineConfigFieldDescription_disable_engine', 'stubEngine2')
}
disable_engine: {
descriptionText: getMessage('EngineConfigFieldDescription_disable_engine', 'stubEngine2'),
valueType: "boolean",
defaultValue: false,
wasSuppliedByUser: false
}
},
});
});

Expand All @@ -220,8 +235,18 @@ describe("Tests for adding engines to Code Analyzer", () => {
expect(codeAnalyzer.getEngineConfigDescription('stubEngine1')).toEqual({
overview: "OverviewForStub1",
fieldDescriptions: {
disable_engine: getMessage('EngineConfigFieldDescription_disable_engine', 'stubEngine1'),
miscSetting1: "someDescriptionFor_miscSetting1"
disable_engine: {
descriptionText: getMessage('EngineConfigFieldDescription_disable_engine', 'stubEngine1'),
valueType: "boolean",
defaultValue: false,
wasSuppliedByUser: true
},
misc_value: {
descriptionText: "someDescriptionFor_misc_value",
valueType: "number",
defaultValue: 1987,
wasSuppliedByUser: true
}
}
});
});
Expand Down
47 changes: 42 additions & 5 deletions packages/code-analyzer-core/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {CodeAnalyzerConfig, SeverityLevel} from "../src";
import * as os from "node:os";
import * as path from "node:path";
import {ConfigDescription, getMessageFromCatalog, SHARED_MESSAGE_CATALOG} from "@salesforce/code-analyzer-engine-api";
import {getMessageFromCatalog, SHARED_MESSAGE_CATALOG} from "@salesforce/code-analyzer-engine-api";
import {getMessage} from "../src/messages";
import {changeWorkingDirectoryToPackageRoot} from "./test-helpers";
import {TOP_LEVEL_CONFIG_DESCRIPTION, DEFAULT_CONFIG} from "../src/config";
import {ConfigDescription, DEFAULT_CONFIG} from "../src/config";

changeWorkingDirectoryToPackageRoot();

Expand Down Expand Up @@ -273,8 +273,45 @@ describe("Tests for creating and accessing configuration values", () => {
'engines.stubEngine1.disable_engine', 'boolean', 'number'));
});

it("When getConfigDescription is called, then it returns our expected description object", () => {
const configDescription: ConfigDescription = CodeAnalyzerConfig.getConfigDescription();
expect(configDescription).toEqual(TOP_LEVEL_CONFIG_DESCRIPTION);
it("When getConfigDescription is called from default config, then it returns our expected description object", () => {
const configDescription: ConfigDescription = CodeAnalyzerConfig.withDefaults().getConfigDescription();
expect(configDescription.overview).toEqual(getMessage('ConfigOverview'));
expect(configDescription.fieldDescriptions).toEqual({
config_root: {
descriptionText: getMessage('ConfigFieldDescription_config_root'),
valueType: 'string',
defaultValue: null,
wasSuppliedByUser: false
},
log_folder: {
descriptionText: getMessage('ConfigFieldDescription_log_folder'),
valueType: 'string',
defaultValue: null,
wasSuppliedByUser: false
},
rules: {
descriptionText: getMessage('ConfigFieldDescription_rules'),
valueType: 'object',
defaultValue: {},
wasSuppliedByUser: false
},
engines: {
descriptionText: getMessage('ConfigFieldDescription_engines'),
valueType: 'object',
defaultValue: {},
wasSuppliedByUser: false
}
});
});

it("When getConfigDescription is called from modified config, then it correctly sets wasSuppliedByUser fields", () => {
const configDescription: ConfigDescription = CodeAnalyzerConfig.fromObject(
{config_root: __dirname, rules: {"someEngine": {"abc": {severity: SeverityLevel.High}}}}
).getConfigDescription();
expect(configDescription.overview).toEqual(getMessage('ConfigOverview'));
expect(configDescription.fieldDescriptions.config_root.wasSuppliedByUser).toEqual(true);
expect(configDescription.fieldDescriptions.log_folder.wasSuppliedByUser).toEqual(false);
expect(configDescription.fieldDescriptions.rules.wasSuppliedByUser).toEqual(true);
expect(configDescription.fieldDescriptions.engines.wasSuppliedByUser).toEqual(false);
});
});
6 changes: 5 additions & 1 deletion packages/code-analyzer-core/test/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export class StubEnginePlugin extends engApi.EnginePluginV1 {
return {
overview: 'OverviewForStub1',
fieldDescriptions: {
miscSetting1: "someDescriptionFor_miscSetting1"
misc_value: {
descriptionText: "someDescriptionFor_misc_value",
valueType: "number",
defaultValue: 1987
}
}
}
}
Expand Down
22 changes: 20 additions & 2 deletions packages/code-analyzer-engine-api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,26 @@ export type ConfigDescription = {
// A brief overview of the configuration. It is recommended to include a link to documentation when possible.
overview?: string

// Description messages of the top-level fields in the configuration
fieldDescriptions?: Record<string, string>
// Description objects for the primary fields in the configuration
fieldDescriptions?: Record<string, ConfigFieldDescription>
}

export type ConfigFieldDescription = {
// Text that describes the field to be documented
descriptionText: string

// The type for the value associated with this field (as a string).
// Note that this as a string instead of an enum in order to give flexibility to describe the type of the field as
// we want it to show up in our public docs, but it is recommended to return one of the following when possible:
// 'string', 'number', 'boolean', 'object', 'array'
valueType: string

// The default value that is to be documented. Use null if you do not have a fixed default value.
// Note that this value may not necessarily be the same that could be auto calculated if the user doesn't provide
// the value in their configuration file. For example, we may want to report null as the default value for a
// java_command field whenever the user doesn't explicitly provide one even though it might automatically
// get calculated at runtime to be some java command found in their environment.
defaultValue: ConfigValue
}

export class ConfigValueExtractor {
Expand Down
1 change: 1 addition & 0 deletions packages/code-analyzer-engine-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
ConfigDescription,
ConfigFieldDescription,
ConfigObject,
ConfigValue,
ConfigValueExtractor,
Expand Down
Loading
Loading