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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

Use our side bar or the **Command Palette** and type `Flow Scanner` to see the list of all available commands.

* `Configure Rules` Allows to define rules and expressions as per defined in the [core documentation](https://github.com/Flow-Scanner/lightning-flow-scanner-core).
* `Scan Flows` allows choosing either a directory or a selection of flows to run the analysis against.
*More information on the default rules can be found in the [core documentation](https://github.com/Flow-Scanner/lightning-flow-scanner-core).*
* `Fix Flows` will apply available fixes automatically.
* `Open Documentation` can be used to reference the documentation.

Expand All @@ -23,7 +23,6 @@ Use our side bar or the **Command Palette** and type `Flow Scanner` to see the l
| `lightningFlowScanner.SpecifyFiles` | Specify flow file paths instead of a root directory. | `false` |
| `lightningFlowScanner.NamingConvention` | Specify a REGEX expression to use as Flow Naming convention. | `"[A-Za-z0-9]+_[A-Za-z0-9]+"` |
| `lightningFlowScanner.APIVersion` | Specify an expression to validate the API version, i.e. '===50'(use at least 50). | `">50"` |
| `lightningFlowScanner.Reset` | Reset all configurations on every scan | `false` |

## Development Setup

Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

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

12 changes: 4 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"icon": "media/lightningflow.png",
"description": "A VS Code Extension for analysis and optimization of Salesforce Flows. Scans metadata for 20+ issues such as hardcoded IDs, unsafe contexts, inefficient SOQL/DML operations, recursion risks, and missing fault handling. Supports auto-fixes, rule configurations, and tests integration.",
"version": "1.7.2",
"version": "1.8.0",
"engines": {
"vscode": "^1.99.1"
},
Expand Down Expand Up @@ -37,11 +37,6 @@
"type": "string",
"default": ">50",
"description": "Specify an expression to validate the API version, i.e. '===50'(use at least 50)."
},
"lightningFlowScanner.Reset": {
"type": "boolean",
"default": false,
"description": "Reset all configurations on every scan"
}
}
},
Expand Down Expand Up @@ -180,7 +175,7 @@
},
"dependencies": {
"convert-array-to-csv": "^2.0.0",
"lightning-flow-scanner-core": "^5.9.0",
"lightning-flow-scanner-core": "5.9.4",
"tabulator-tables": "^6.3.1",
"uuid": "^11.0.5",
"xml2js": "^0.6.2",
Expand All @@ -195,6 +190,7 @@
"flow scanner",
"salesforce flow",
"best practices",
"code quality"
"code quality",
"salesforce automation"
]
}
185 changes: 127 additions & 58 deletions src/commands/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CacheProvider } from '../providers/cache-provider';
import { testdata } from '../store/testdata';
import { OutputChannel } from '../providers/outputChannel';
import { ConfigProvider } from '../providers/config-provider';
import * as YAML from 'yaml';

const { USE_NEW_CONFIG: isUseNewConfig } = process.env;

Expand Down Expand Up @@ -40,83 +41,151 @@ export default class Commands {
vscode.env.openExternal(url);
}

private async configRules() {
if (isUseNewConfig === 'true') {
await this.ruleConfiguration();
private async configRules() {
type RuleWithExpression = { severity: string; expression?: string };
type RuleConfig = Record<string, RuleWithExpression>;

// If new YAML config flow is enabled
if (isUseNewConfig === 'true') {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage('No workspace folder found.');
return;
}
const allRules: core.IRuleDefinition[] = [
...core.getRules()
];
const ruleConfig = { rules: {} };

let items = allRules.map((rule) => {
return { label: rule.label, value: rule.name, picked: true };
});
const workspacePath = workspaceFolders[0].uri.fsPath;
const configProvider = new ConfigProvider();

try {
const configResult = await configProvider.discover(workspacePath);

// Type assertion for safe access
const configTyped = configResult.config as { rules?: RuleConfig } | undefined;
const rules: RuleConfig = configTyped?.rules ?? {};

const selectedRules = (await vscode.window.showQuickPick(items, {
canPickMany: true,
})) as { label: string; value: string }[];
// Prompt for missing expressions
if (!rules.FlowName?.expression) {
const naming = await vscode.window.showInputBox({
prompt: 'Define a naming convention for Flow Names (REGEX format).',
placeHolder: '[A-Za-z0-9]+_[A-Za-z0-9]+',
value: '[A-Za-z0-9]+_[A-Za-z0-9]+',
});
if (naming) rules.FlowName = { severity: 'error', expression: naming };
}

for (const rule of allRules) {
if (selectedRules.map((r) => r.value).includes(rule.name)) {
ruleConfig.rules[rule.name] = { severity: 'error' };
if (!rules.APIVersion?.expression) {
const apiVersion = await vscode.window.showInputBox({
prompt: 'Set an API Version rule (e.g. ">50" or ">=60").',
placeHolder: '>50',
value: '>50',
});
if (apiVersion) rules.APIVersion = { severity: 'error', expression: apiVersion };
}

// Persist updated YAML config using yaml.stringify
const yamlString = YAML.stringify(configResult.config);
await vscode.workspace.fs.writeFile(
vscode.Uri.file(configResult.fspath),
new TextEncoder().encode(yamlString)
);

// Store rules in cache
await CacheProvider.instance.set('ruleconfig', rules);
OutputChannel.getInstance().logChannel.debug('Stored YAML rule config', rules);

// Open config file in editor
const document = await vscode.workspace.openTextDocument(configResult.fspath);
await vscode.window.showTextDocument(document);
vscode.window.showInformationMessage(`Loaded configuration from ${configResult.fspath}`);
return;
} catch (err: any) {
vscode.window.showErrorMessage(`Error loading configuration: ${err?.message || err}`);
return;
}
}

// ----- Legacy QuickPick flow -----
const allRules: core.IRuleDefinition[] = [...core.getRules()];
const ruleConfig: RuleConfig = {};

const items = allRules.map((rule) => ({
label: rule.label,
value: rule.name,
picked: true,
}));

const selectedRules = (await vscode.window.showQuickPick(items, {
canPickMany: true,
})) as { label: string; value: string }[];

if (!selectedRules) {
vscode.window.showInformationMessage('No rules selected.');
return;
}

for (const rule of allRules) {
if (selectedRules.map((r) => r.value).includes(rule.name)) {
ruleConfig[rule.name] = { severity: 'error' };
}
if (selectedRules.map((r) => r.value).includes('FlowName')) {
const namingConventionString = await vscode.window.showInputBox({
prompt:
'Readability of a flow is very important. Setting a naming convention for the Flow Name will improve the findability/searchability and overall consistency. You can define your default naming convention using REGEX.',
placeHolder: '[A-Za-z0-9]+_[A-Za-z0-9]+',
value: '[A-Za-z0-9]+_[A-Za-z0-9]+',
});
ruleConfig.rules['FlowName'] = {
severity: 'error',
expression: namingConventionString,
};
}

// Prompt for FlowName expression if selected
if (selectedRules.some((r) => r.value === 'FlowName')) {
const naming = await vscode.window.showInputBox({
prompt: 'Define a naming convention for Flow Names (REGEX format).',
placeHolder: '[A-Za-z0-9]+_[A-Za-z0-9]+',
value: '[A-Za-z0-9]+_[A-Za-z0-9]+',
});
if (naming) {
ruleConfig['FlowName'] = { severity: 'error', expression: naming };
await vscode.workspace
.getConfiguration()
.update(
'lightningFlowScanner.NamingConvention',
namingConventionString,
true
);
.update('lightningFlowScanner.NamingConvention', naming, true);
}
if (selectedRules.map((r) => r.value).includes('APIVersion')) {
const apiVersionEvalExpressionString = await vscode.window.showInputBox({
prompt:
' The Api Version has been available as an attribute on the Flow since API v50.0 and it is recommended to limit variation and to update them on a regular basis. Set an expression to set a valid range of API versions(Minimum 50).',
placeHolder: '>50',
value: '>50',
});
ruleConfig.rules['APIVersion'] = {
severity: 'error',
expression: apiVersionEvalExpressionString,
};
}

// Prompt for APIVersion expression if selected
if (selectedRules.some((r) => r.value === 'APIVersion')) {
const apiVersion = await vscode.window.showInputBox({
prompt: 'Set an API Version rule (e.g. ">50" or ">=60").',
placeHolder: '>50',
value: '>50',
});
if (apiVersion) {
ruleConfig['APIVersion'] = { severity: 'error', expression: apiVersion };
await vscode.workspace
.getConfiguration()
.update(
'lightningFlowScanner.APIVersion',
apiVersionEvalExpressionString,
true
);
.update('lightningFlowScanner.APIVersion', apiVersion, true);
}
await CacheProvider.instance.set('ruleconfig', ruleConfig);
OutputChannel.getInstance().logChannel.debug(
'Stored rule configurations',
ruleConfig
);
}

// Store legacy rules in cache
await CacheProvider.instance.set('ruleconfig', ruleConfig);
OutputChannel.getInstance().logChannel.debug('Stored legacy rule config', ruleConfig);
}


private async ruleConfiguration() {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage('No workspace folder found.');
return;
}

const workspacePath = workspaceFolders[0].uri.fsPath;
const configProvider = new ConfigProvider();
const config = await configProvider.discover(
vscode.workspace.workspaceFolders?.[0].uri.path
);
const document = await vscode.workspace.openTextDocument(config.fspath);
await vscode.window.showTextDocument(document);

try {
const config = await configProvider.discover(workspacePath);
const document = await vscode.workspace.openTextDocument(config.fspath);
await vscode.window.showTextDocument(document);
vscode.window.showInformationMessage(`Loaded configuration from ${config.fspath}`);
} catch (err: any) {
vscode.window.showErrorMessage(`Error loading configuration: ${err?.message || err}`);
}
}


private async debugView() {
let results = testdata as unknown as core.ScanResult[];
await CacheProvider.instance.set('results', results);
Expand Down
Loading
Loading