@@ -16,6 +16,7 @@ interface RuleEntry {
1616 severity : string ;
1717 expression ?: string ;
1818}
19+
1920type RuleConfig = Record < string , RuleEntry > ;
2021
2122export 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