@@ -4,6 +4,8 @@ open Argu
44open System
55open System.IO
66open System.Reflection
7+ open System.Linq
8+ open System.Text
79open FSharpLint.Framework
810open FSharpLint.Application
911
@@ -23,19 +25,25 @@ type internal FileType =
2325type ExitCode =
2426 | Failure = - 1
2527 | Success = 0
28+ | NoSuchRuleName = 1
29+ | NoSuggestedFix = 2
30+
31+ let fileTypeHelp = " Input type the linter will run against. If this is not set, the file type will be inferred from the file extension."
2632
2733// Allowing underscores in union case names for proper Argu command line option formatting.
2834// fsharplint:disable UnionCasesNames
2935type private ToolArgs =
3036 | [<AltCommandLine( " -f" ) >] Format of OutputFormat
3137 | [<CliPrefix( CliPrefix.None) >] Lint of ParseResults < LintArgs >
38+ | [<CliPrefix( CliPrefix.None) >] Fix of ParseResults < FixArgs >
3239 | Version
3340with
3441 interface IArgParserTemplate with
3542 member this.Usage =
3643 match this with
3744 | Format _ -> " Output format of the linter."
3845 | Lint _ -> " Runs FSharpLint against a file or a collection of files."
46+ | Fix _ -> " Apply quickfixes for specified rule name or names (comma separated)."
3947 | Version -> " Prints current version."
4048
4149// TODO: investigate erroneous warning on this type definition
5058 member this.Usage =
5159 match this with
5260 | Target _ -> " Input to lint."
53- | File_ Type _ -> " Input type the linter will run against. If this is not set, the file type will be inferred from the file extension. "
61+ | File_ Type _ -> fileTypeHelp
5462 | Lint_ Config _ -> " Path to the config for the lint."
63+
64+ // TODO: investigate erroneous warning on this type definition
65+ // fsharplint:disable UnionDefinitionIndentation
66+ and private FixArgs =
67+ | [<MainCommand; Mandatory>] Fix_ Target of ruleName : string * target : string
68+ | Fix_ File_ Type of FileType
69+ // fsharplint:enable UnionDefinitionIndentation
70+ with
71+ interface IArgParserTemplate with
72+ member this.Usage =
73+ match this with
74+ | Fix_ Target _ -> " Rule name to be applied with suggestedFix and input to lint."
75+ | Fix_ File_ Type _ -> fileTypeHelp
5576// fsharplint:enable UnionCasesNames
5677
78+ type private LintingArgs =
79+ {
80+ FileType: FileType
81+ LintParams: OptionalLintParameters
82+ Target: string
83+ ToolsPath: Ionide .ProjInfo .Types .ToolsPath
84+ ShouldFix: bool
85+ MaybeRuleName: string option
86+ }
87+
5788/// Expands a wildcard pattern to a list of matching files.
5889/// Supports recursive search using ** (e.g., "** /* .fs" or "src/** /* .fs")
5990let internal expandWildcard ( pattern : string ) =
@@ -117,69 +148,158 @@ let internal inferFileType (target:string) =
117148 else
118149 FileType.Source
119150
120- let private lint
121- ( lintArgs : ParseResults < LintArgs >)
122- ( output : Output.IOutput )
123- ( toolsPath : Ionide.ProjInfo.Types.ToolsPath )
124- : ExitCode =
125- let mutable exitCode = ExitCode.Success
126-
127- let handleError ( str : string ) =
128- output.WriteError str
129- exitCode <- ExitCode.Failure
130-
131- let handleLintResult = function
132- | LintResult.Success( warnings) ->
133- String.Format( Resources.GetString( " ConsoleFinished" ), List.length warnings)
134- |> output.WriteInfo
135- if not ( List.isEmpty warnings) then
136- exitCode <- ExitCode.Failure
137- | LintResult.Failure( failure) ->
138- handleError failure.Description
139-
140- let lintConfig = lintArgs.TryGetResult Lint_ Config
141-
142- let configParam =
143- match lintConfig with
151+ let private outputWarnings ( output : Output.IOutput ) ( warnings : List < Suggestion.LintWarning >) =
152+ String.Format( Resources.GetString " ConsoleFinished" , List.length warnings)
153+ |> output.WriteInfo
154+
155+ let private handleLintResult ( output : Output.IOutput ) ( lintResult : LintResult ) =
156+ match lintResult with
157+ | LintResult.Success warnings ->
158+ outputWarnings output warnings
159+ if List.isEmpty warnings |> not then
160+ ExitCode.Failure
161+ else
162+ ExitCode.Success
163+ | LintResult.Failure failure ->
164+ output.WriteError failure.Description
165+ ExitCode.Failure
166+
167+ let private getParams ( output : Output.IOutput ) config =
168+ let paramConfig =
169+ match config with
144170 | Some configPath -> FromFile configPath
145171 | None -> Default
146172
147- let lintParams =
148- {
149- CancellationToken = None
150- ReceivedWarning = Some output.WriteWarning
151- Configuration = configParam
152- ReportLinterProgress = Some ( parserProgress output)
153- }
173+ { CancellationToken = None
174+ ReceivedWarning = Some output.WriteWarning
175+ Configuration = paramConfig
176+ ReportLinterProgress = parserProgress output |> Some }
154177
155- let target = lintArgs.GetResult Target
156- let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
178+ let private handleFixResult ( output : Output.IOutput ) ( target : string ) ( ruleName : string ) ( lintResult : LintResult ) : ExitCode =
179+ match lintResult with
180+ | LintResult.Success warnings ->
181+ String.Format( Resources.GetString " ConsoleApplyingSuggestedFixFile" , target) |> output.WriteInfo
182+ let increment = 1
183+ let noFixIncrement = 0
157184
185+ let countFixes ( element : Suggestion.LintWarning ) =
186+ let sourceCode = File.ReadAllText element.FilePath
187+ if String.Equals( ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
188+ match element.Details.SuggestedFix with
189+ | Some lazySuggestedFix ->
190+ match lazySuggestedFix.Force() with
191+ | Some suggestedFix ->
192+ let updatedSourceCode =
193+ let builder = StringBuilder( sourceCode.Length + suggestedFix.ToText.Length)
194+ let firstPart =
195+ sourceCode.AsSpan(
196+ 0 ,
197+ ( ExpressionUtilities.findPos suggestedFix.FromRange.Start sourceCode)
198+ |> Option.defaultWith
199+ ( fun () -> failwith " Could not find index from position (suggestedFix.FromRange.Start)" )
200+ )
201+ let secondPart =
202+ sourceCode.AsSpan
203+ ( ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode
204+ |> Option.defaultWith
205+ ( fun () -> failwith " Could not find index from position (suggestedFix.FromRange.End)" ))
206+ builder
207+ .Append( firstPart)
208+ .Append( suggestedFix.ToText)
209+ .Append( secondPart)
210+ .ToString()
211+ File.WriteAllText(
212+ element.FilePath,
213+ updatedSourceCode,
214+ Encoding.UTF8)
215+ | _ -> ()
216+ increment
217+ | None -> noFixIncrement
218+ else
219+ noFixIncrement
220+
221+ let countSuggestedFix =
222+ warnings |> List.sumBy countFixes
223+ outputWarnings output warnings
224+
225+ if countSuggestedFix > 0 then
226+ ExitCode.Success
227+ else
228+ ExitCode.NoSuggestedFix
229+
230+ | LintResult.Failure failure ->
231+ output.WriteError failure.Description
232+ ExitCode.Failure
233+
234+ let private linting ( output : Output.IOutput ) ( args : LintingArgs ) =
158235 try
159236 let lintResult =
160- match fileType with
161- | FileType.File -> Lint.lintFile lintParams target
162- | FileType.Source -> Lint.lintSource lintParams target
163- | FileType.Solution -> Lint.lintSolution lintParams target toolsPath
164- | FileType.Wildcard ->
165- output.WriteInfo " Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues."
166- let files = expandWildcard target
167- if List.isEmpty files then
168- output.WriteInfo $" No files matching pattern '%s {target}' were found."
169- LintResult.Success List.empty
170- else
171- output.WriteInfo $" Found %d {List.length files} file(s) matching pattern '%s {target}'."
172- Lint.lintFiles lintParams files
237+ match args.FileType with
238+ | FileType.File -> Lint.lintFile args.LintParams args.Target
239+ | FileType.Source -> Lint.lintSource args.LintParams args.Target
240+ | FileType.Solution -> Lint.lintSolution args.LintParams args.Target args.ToolsPath
173241 | FileType.Project
174- | _ -> Lint.lintProject lintParams target toolsPath
175- handleLintResult lintResult
242+ | _ -> Lint.lintProject args.LintParams args.Target args.ToolsPath
243+ if args.ShouldFix then
244+ match args.MaybeRuleName with
245+ | Some ruleName -> handleFixResult output args.Target ruleName lintResult
246+ | None -> ExitCode.NoSuchRuleName
247+ else
248+ handleLintResult output lintResult
176249 with
177250 | exn ->
178- let target = if fileType = FileType.Source then " source" else target
179- handleError
180- $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
251+ let target = if args.FileType = FileType.Source then " source" else args.Target
252+ output.WriteError $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
253+ ExitCode.Failure
254+
255+ let private applyLint ( lintArgs : ParseResults < LintArgs >) ( output : Output.IOutput ) ( toolsPath : Ionide.ProjInfo.Types.ToolsPath ) : ExitCode =
256+ let lintConfig = lintArgs.TryGetResult Lint_ Config
181257
182- exitCode
258+ let lintParams = getParams output lintConfig
259+ let target = lintArgs.GetResult Target
260+ let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
261+
262+ linting
263+ output
264+ { FileType = fileType
265+ LintParams = lintParams
266+ Target = target
267+ ToolsPath = toolsPath
268+ ShouldFix = false
269+ MaybeRuleName = None }
270+
271+ let private applySuggestedFix ( fixArgs : ParseResults < FixArgs >) ( output : Output.IOutput ) toolsPath =
272+ let fixParams = getParams output None
273+ let ruleName , target = fixArgs.GetResult Fix_ Target
274+ let fileType = fixArgs.TryGetResult Fix_ File_ Type |> Option.defaultValue ( inferFileType target)
275+
276+ let allRules =
277+ match getConfig fixParams.Configuration with
278+ | Ok config -> Some ( Configuration.flattenConfig config false )
279+ | _ -> None
280+
281+ let allRuleNames =
282+ match allRules with
283+ | Some rules -> ( fun ( loadedRules : Configuration.LoadedRules ) -> ([|
284+ loadedRules.LineRules.IndentationRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
285+ loadedRules.LineRules.NoTabCharactersRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
286+ loadedRules.LineRules.GenericLineRules |> Array.map ( fun rule -> rule.Name)
287+ loadedRules.AstNodeRules |> Array.map ( fun rule -> rule.Name)
288+ |] |> Array.concat |> Set.ofArray)) rules
289+ | _ -> Set.empty
290+
291+ if allRuleNames.Any( fun aRuleName -> String.Equals( aRuleName, ruleName, StringComparison.InvariantCultureIgnoreCase)) then
292+ linting
293+ output
294+ { FileType = fileType
295+ LintParams = fixParams
296+ Target = target
297+ ToolsPath = toolsPath
298+ ShouldFix = true
299+ MaybeRuleName = Some ruleName }
300+ else
301+ output.WriteError <| sprintf " Rule '%s ' does not exist." ruleName
302+ ExitCode.NoSuchRuleName
183303
184304let private start ( arguments : ParseResults < ToolArgs >) ( toolsPath : Ionide.ProjInfo.Types.ToolsPath ) =
185305 let output =
@@ -202,7 +322,9 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
202322
203323 match arguments.GetSubCommand() with
204324 | Lint lintArgs ->
205- lint lintArgs output toolsPath
325+ applyLint lintArgs output toolsPath
326+ | Fix fixArgs ->
327+ applySuggestedFix fixArgs output toolsPath
206328 | _ ->
207329 ExitCode.Failure
208330
0 commit comments