Skip to content

Commit 5fc4e23

Browse files
committed
adds beta opt-in in configuration
1 parent 42cd6d1 commit 5fc4e23

File tree

2 files changed

+118
-130
lines changed

2 files changed

+118
-130
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

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

16-
* `Configure Flow Scanner` - Set up rules in `.flow-scanner.yml` (see [documentation](https://github.com/Flow-Scanner/lightning-flow-scanner-core))
16+
* `Configure Flow Scanner` - Set up rules in `.flow-scanner.yml` (see [scanner documentation](https://github.com/Flow-Scanner/lightning-flow-scanner-core))
1717
* `Scan Flows` - Analyze a directory or selected flow files
1818
* `Fix Flows` - Automatically apply available fixes
1919
* `Flow Scanner Documentation` - Open the rules reference guide
@@ -37,7 +37,8 @@ It is recommended to set up a `.flow-scanner.yml` and define:
3737
},
3838
"exceptions": {
3939
// Your exceptions here
40-
}
40+
},
41+
"betamode": false // include rules currently in beta
4142
}
4243
```
4344

@@ -119,7 +120,7 @@ If you’re developing or testing updates to the core module, you can link it lo
119120

120121
## VSCE to VSX
121122

122-
The `lightning-flow-scanner-vsce` package was unpublished from the Visual Studio and Open VSX Marketplaces due to a vulnerability stemming from unsafe rule loading. The issue was addressed in [v5 of the core library](https://github.com/Flow-Scanner/lightning-flow-scanner-core/releases/tag/v5.1.0). This fork, created on 22/09/2025, emphasizes security and maintainability.
123+
The `lightning-flow-scanner-vsce` package was unpublished from the Visual Studio and Open VSX Marketplaces due to a vulnerability stemming from unsafe rule loading. The issue was addressed in [core library v5](https://github.com/Flow-Scanner/lightning-flow-scanner-core/releases/tag/v5.1.0). This fork, created on 22/09/2025, emphasizes security and maintainability.
123124

124125
<p><strong>Want to help improve Lightning Flow Scanner? See our <a href="https://github.com/Flow-Scanner/lightning-flow-scanner-core?tab=contributing-ov-file">Contributing Guidelines</a></strong></p>
125126
<!-- force-contributors-render: 2025-10-28 22:10:01 -->

src/commands/handlers.ts

Lines changed: 114 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface RuleEntry {
1616
severity: string;
1717
expression?: string;
1818
}
19+
1920
type RuleConfig = Record<string, RuleEntry>;
2021

2122
export default class Commands {
@@ -38,16 +39,11 @@ export default class Commands {
3839
vscode.env.openExternal(url);
3940
}
4041

41-
private async loadConfig(workspacePath: string): Promise<RuleConfig> {
42+
private async loadConfig(workspacePath: string): Promise<{ rules: RuleConfig; betamode: boolean }> {
4243
const rawConfig = await loadScannerConfig(workspacePath);
4344
// OutputChannel.getInstance().logChannel.debug('Raw config loaded:', JSON.stringify(rawConfig, null, 2));
44-
4545
const rawRules = (rawConfig.rules as Record<string, unknown>) || {};
46-
47-
// todo implement beta
48-
4946
const rules: RuleConfig = {};
50-
5147
for (const [name, rule] of Object.entries(rawRules)) {
5248
if (typeof rule === 'object' && rule !== null) {
5349
const r = rule as Record<string, unknown>;
@@ -57,138 +53,139 @@ export default class Commands {
5753
};
5854
}
5955
}
60-
61-
await CacheProvider.instance.set('ruleconfig', rules);
62-
return rules;
56+
const betamode = !!rawConfig.betamode;
57+
await CacheProvider.instance.set('ruleconfig', { rules, betamode });
58+
return { rules, betamode };
6359
}
6460

65-
private async saveConfig(workspacePath: string, rules: RuleConfig) {
61+
private async saveConfig(workspacePath: string, rules: RuleConfig, betamode: boolean) {
6662
const configPath = path.join(workspacePath, '.flow-scanner.yml');
67-
const config = { rules };
68-
const yamlLines = ['rules:'];
69-
for (const [name, rule] of Object.entries(config.rules)) {
70-
yamlLines.push(` ${name}:`); // 2 spaces
71-
yamlLines.push(` severity: ${rule.severity}`); // 4 spaces
63+
const yamlLines: string[] = [];
64+
if (betamode) {
65+
yamlLines.push('betamode: true');
66+
}
67+
yamlLines.push('rules:');
68+
for (const [name, rule] of Object.entries(rules)) {
69+
yamlLines.push(` ${name}:`); // 2 spaces
70+
yamlLines.push(` severity: ${rule.severity}`); // 4 spaces
7271
if (rule.expression) {
73-
yamlLines.push(` expression: ${JSON.stringify(rule.expression)}`); // 4 spaces
72+
yamlLines.push(` expression: ${JSON.stringify(rule.expression)}`); // 4 spaces
7473
}
7574
}
7675
const yamlString = yamlLines.join('\n');
7776
await vscode.workspace.fs.writeFile(vscode.Uri.file(configPath), new TextEncoder().encode(yamlString));
78-
await CacheProvider.instance.set('ruleconfig', rules);
77+
await CacheProvider.instance.set('ruleconfig', { rules, betamode });
7978
}
8079

81-
private async configRules() {
82-
const ws = vscode.workspace.workspaceFolders?.[0];
83-
if (!ws) {
84-
vscode.window.showErrorMessage('No workspace folder found.');
85-
return;
86-
}
87-
const workspacePath = ws.uri.fsPath;
88-
const configPath = path.join(workspacePath, '.flow-scanner.yml');
89-
90-
// Check if config file exists and offer to open it
91-
try {
92-
await vscode.workspace.fs.stat(vscode.Uri.file(configPath));
93-
// File exists - ask user what they want to do
94-
const choice = await vscode.window.showQuickPick(
95-
['Open Config File', 'Reconfigure Rules'],
96-
{
97-
placeHolder: 'Configuration file exists. What would you like to do?'
98-
}
99-
);
100-
101-
if (choice === undefined) return;
102-
103-
if (choice === 'Open Config File') {
104-
const doc = await vscode.workspace.openTextDocument(configPath);
105-
await vscode.window.showTextDocument(doc);
80+
private async configRules() {
81+
const ws = vscode.workspace.workspaceFolders?.[0];
82+
if (!ws) {
83+
vscode.window.showErrorMessage('No workspace folder found.');
10684
return;
10785
}
108-
// Otherwise continue with reconfiguration
109-
} catch {
110-
// File doesn't exist, continue with normal flow
111-
}
112-
113-
let rules: RuleConfig = await this.loadConfig(workspacePath);
114-
const allRules = [...core.getRules()];
115-
const currentNames = Object.keys(rules);
116-
117-
// Preselect all rules if no config exists
118-
const isEmptyConfig = currentNames.length === 0;
119-
const items = allRules.map(rule => ({
120-
label: rule.label,
121-
description: rule.name,
122-
picked: isEmptyConfig ? true : currentNames.includes(rule.name),
123-
}));
124-
const selected = await vscode.window.showQuickPick(items, {
125-
canPickMany: true,
126-
placeHolder: 'Select rules to enable/disable',
127-
});
128-
if (selected === undefined) return;
129-
const newRules: RuleConfig = {};
130-
for (const item of selected) {
131-
const def = allRules.find(r => r.name === item.description)!;
132-
const existing = rules[def.name];
133-
newRules[def.name] = {
134-
severity: existing?.severity || 'error',
135-
expression: existing?.expression,
136-
};
137-
}
138-
let changed = false;
139-
if (newRules.FlowName) {
140-
const current = newRules.FlowName.expression || '';
141-
const expr = await vscode.window.showInputBox({
142-
prompt: 'Define naming convention (REGEX) for FlowName',
143-
placeHolder: '[A-Za-z0-9]+_[A-Za-z0-9]+',
144-
value: current || '[A-Za-z0-9]+_[A-Za-z0-9]+',
145-
});
146-
if (expr !== undefined && expr.trim() !== current) {
147-
newRules.FlowName.expression = expr.trim() || undefined;
148-
changed = true;
86+
const workspacePath = ws.uri.fsPath;
87+
const configPath = path.join(workspacePath, '.flow-scanner.yml');
88+
// Check if config file exists and offer to open it
89+
try {
90+
await vscode.workspace.fs.stat(vscode.Uri.file(configPath));
91+
// File exists - ask user what they want to do
92+
const choice = await vscode.window.showQuickPick(
93+
['Open Config File', 'Reconfigure Rules'],
94+
{
95+
placeHolder: 'Configuration file exists. What would you like to do?'
96+
}
97+
);
98+
if (choice === undefined) return;
99+
if (choice === 'Open Config File') {
100+
const doc = await vscode.workspace.openTextDocument(configPath);
101+
await vscode.window.showTextDocument(doc);
102+
return;
103+
}
104+
// Otherwise continue with reconfiguration
105+
} catch {
106+
// File doesn't exist, continue with normal flow
149107
}
150-
}
151-
if (newRules.APIVersion) {
152-
const current = newRules.APIVersion.expression || '';
153-
const expr = await vscode.window.showInputBox({
154-
prompt: 'Set API version rule (e.g. ">=50")',
155-
placeHolder: '>=50',
156-
value: current || '>=50',
108+
const config = await this.loadConfig(workspacePath);
109+
let rules = config.rules;
110+
let currentBetamode = config.betamode;
111+
const betaOptions = currentBetamode ? ['Yes', 'No'] : ['No', 'Yes'];
112+
const includeBeta = await vscode.window.showQuickPick(betaOptions, {
113+
placeHolder: 'Do you want to opt-in for beta rules?'
157114
});
158-
if (expr !== undefined && expr.trim() !== current) {
159-
newRules.APIVersion.expression = expr.trim() || undefined;
160-
changed = true;
115+
if (includeBeta === undefined) return;
116+
const betamode = includeBeta === 'Yes';
117+
const allRules = [...core.getRules(), ...(betamode ? core.getBetaRules() : [])];
118+
const currentNames = Object.keys(rules);
119+
// Preselect all rules if no config exists
120+
const isEmptyConfig = currentNames.length === 0;
121+
const items = allRules.map(rule => ({
122+
label: rule.label,
123+
description: rule.name,
124+
picked: isEmptyConfig ? true : currentNames.includes(rule.name),
125+
}));
126+
const selected = await vscode.window.showQuickPick(items, {
127+
canPickMany: true,
128+
placeHolder: 'Select rules to enable/disable',
129+
});
130+
if (selected === undefined) return;
131+
const newRules: RuleConfig = {};
132+
for (const item of selected) {
133+
const def = allRules.find(r => r.name === item.description)!;
134+
const existing = rules[def.name];
135+
newRules[def.name] = {
136+
severity: existing?.severity || 'error',
137+
expression: existing?.expression,
138+
};
161139
}
162-
}
163-
if (changed || Object.keys(newRules).length !== currentNames.length) {
164-
await this.saveConfig(workspacePath, newRules);
165-
166-
// After saving, offer to open the file
167-
const openFile = await vscode.window.showInformationMessage(
168-
'Configuration saved successfully!',
169-
'Open Config File'
170-
);
171-
if (openFile) {
172-
const doc = await vscode.workspace.openTextDocument(configPath);
173-
await vscode.window.showTextDocument(doc);
140+
let changed = false;
141+
if (newRules.FlowName) {
142+
const current = newRules.FlowName.expression || '';
143+
const expr = await vscode.window.showInputBox({
144+
prompt: 'Define naming convention (REGEX) for FlowName',
145+
placeHolder: '[A-Za-z0-9]+_[A-Za-z0-9]+',
146+
value: current || '[A-Za-z0-9]+_[A-Za-z0-9]+',
147+
});
148+
if (expr !== undefined && expr.trim() !== current) {
149+
newRules.FlowName.expression = expr.trim() || undefined;
150+
changed = true;
151+
}
152+
}
153+
if (newRules.APIVersion) {
154+
const current = newRules.APIVersion.expression || '';
155+
const expr = await vscode.window.showInputBox({
156+
prompt: 'Set API version rule (e.g. ">=50")',
157+
placeHolder: '>=50',
158+
value: current || '>=50',
159+
});
160+
if (expr !== undefined && expr.trim() !== current) {
161+
newRules.APIVersion.expression = expr.trim() || undefined;
162+
changed = true;
163+
}
164+
}
165+
if (changed || Object.keys(newRules).length !== currentNames.length || betamode !== currentBetamode) {
166+
await this.saveConfig(workspacePath, newRules, betamode);
167+
// After saving, offer to open the file
168+
const openFile = await vscode.window.showInformationMessage(
169+
'Configuration saved successfully!',
170+
'Open Config File'
171+
);
172+
if (openFile) {
173+
const doc = await vscode.workspace.openTextDocument(configPath);
174+
await vscode.window.showTextDocument(doc);
175+
}
174176
}
175177
}
176-
}
177178

178179
private async scanFlows() {
179180
const selectedUris = await this.selectFlows('Select flow files or folder to scan:');
180181
if (!selectedUris) return;
181-
182182
const root = vscode.workspace.workspaceFolders![0].uri;
183183
ScanOverview.createOrShow(this.context.extensionUri, []);
184-
185184
const configReset = vscode.workspace.getConfiguration('flowscanner').get<boolean>('Reset');
186185
if (configReset) await this.configRules();
187-
188186
// Load config dynamically from YAML file
189-
const ruleConfig = await this.loadConfig(root.fsPath);
190-
191-
if (Object.keys(ruleConfig).length === 0) {
187+
const config = await this.loadConfig(root.fsPath);
188+
if (Object.keys(config.rules).length === 0) {
192189
const choice = await vscode.window.showWarningMessage(
193190
'No rules configured. Run "Configure Rules" first?',
194191
'Configure Now',
@@ -199,20 +196,16 @@ private async configRules() {
199196
return;
200197
}
201198
}
202-
203-
OutputChannel.getInstance().logChannel.debug('Using rule config for scan:', ruleConfig);
204-
const scanConfig = { rules: ruleConfig };
205-
199+
OutputChannel.getInstance().logChannel.debug('Using rule config for scan:', config);
200+
const scanConfig = { rules: config.rules, betamode: config.betamode };
206201
const parsed = await core.parse(toFsPaths(selectedUris));
207202
const results = core.scan(parsed, scanConfig);
208-
209203
await CacheProvider.instance.set('results', results);
210204
ScanOverview.createOrShow(this.context.extensionUri, results);
211205
}
212206

213207
private async fixFlows() {
214208
let results: core.ScanResult[] = CacheProvider.instance.get('results') || [];
215-
216209
if (results.length > 0) {
217210
const use = await vscode.window.showQuickPick(
218211
['Use last scan results', 'Select different files to fix'],
@@ -221,15 +214,12 @@ private async configRules() {
221214
if (use === 'Select different files to fix') results = [];
222215
else if (use === undefined) return;
223216
}
224-
225217
if (results.length === 0) {
226218
const uris = await this.selectFlows('Select flow files to fix:');
227219
if (!uris) return;
228-
229220
const root = vscode.workspace.workspaceFolders![0].uri;
230-
const ruleConfig = await this.loadConfig(root.fsPath);
231-
232-
if (Object.keys(ruleConfig).length === 0) {
221+
const config = await this.loadConfig(root.fsPath);
222+
if (Object.keys(config.rules).length === 0) {
233223
const choice = await vscode.window.showWarningMessage(
234224
'No rules configured. Run "Configure Rules" first?',
235225
'Configure Now',
@@ -240,17 +230,14 @@ private async configRules() {
240230
return;
241231
}
242232
}
243-
244233
const parsed = await core.parse(toFsPaths(uris));
245-
results = core.scan(parsed, ruleConfig);
234+
results = core.scan(parsed, { rules: config.rules, betamode: config.betamode });
246235
}
247-
248236
if (results.length === 0) {
249237
vscode.window.showInformationMessage('No issues to fix.');
250238
ScanOverview.createOrShow(this.context.extensionUri, []);
251239
return;
252240
}
253-
254241
ScanOverview.createOrShow(this.context.extensionUri, results);
255242
const fixed = core.fix(results);
256243
for (const r of fixed) {

0 commit comments

Comments
 (0)