@@ -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
@@ -19,18 +21,28 @@ type internal FileType =
1921 | File = 3
2022 | Source = 4
2123
24+ type ExitCode =
25+ | Error = - 1
26+ | Success = 0
27+ | NoSuchRuleName = 1
28+ | NoSuggestedFix = 2
29+
30+ let fileTypeHelp = " Input type the linter will run against. If this is not set, the file type will be inferred from the file extension."
31+
2232// Allowing underscores in union case names for proper Argu command line option formatting.
2333// fsharplint:disable UnionCasesNames
2434type private ToolArgs =
2535 | [<AltCommandLine( " -f" ) >] Format of OutputFormat
2636 | [<CliPrefix( CliPrefix.None) >] Lint of ParseResults < LintArgs >
37+ | [<CliPrefix( CliPrefix.None) >] Fix of ParseResults < FixArgs >
2738 | Version
2839with
2940 interface IArgParserTemplate with
3041 member this.Usage =
3142 match this with
3243 | Format _ -> " Output format of the linter."
3344 | Lint _ -> " Runs FSharpLint against a file or a collection of files."
45+ | Fix _ -> " Apply quickfixes for specified rule name or names (comma separated)."
3446 | Version -> " Prints current version."
3547
3648// TODO: investigate erroneous warning on this type definition
4557 member this.Usage =
4658 match this with
4759 | Target _ -> " Input to lint."
48- | File_ Type _ -> " Input type the linter will run against. If this is not set, the file type will be inferred from the file extension. "
60+ | File_ Type _ -> fileTypeHelp
4961 | Lint_ Config _ -> " Path to the config for the lint."
62+
63+ // TODO: investigate erroneous warning on this type definition
64+ // fsharplint:disable UnionDefinitionIndentation
65+ and private FixArgs =
66+ | [<MainCommand; Mandatory>] Fix_ Target of ruleName : string * target : string
67+ | Fix_ File_ Type of FileType
68+ // fsharplint:enable UnionDefinitionIndentation
69+ with
70+ interface IArgParserTemplate with
71+ member this.Usage =
72+ match this with
73+ | Fix_ Target _ -> " Rule name to be applied with suggestedFix and input to lint."
74+ | Fix_ File_ Type _ -> fileTypeHelp
5075// fsharplint:enable UnionCasesNames
5176
5277let private parserProgress ( output : Output.IOutput ) = function
@@ -71,7 +96,7 @@ let internal inferFileType (target:string) =
7196 FileType.Source
7297
7398let private start ( arguments : ParseResults < ToolArgs >) ( toolsPath : Ionide.ProjInfo.Types.ToolsPath ) =
74- let mutable exitCode = 0
99+ let mutable exitCode = ExitCode.Success
75100
76101 let output =
77102 match arguments.TryGetResult Format with
@@ -87,38 +112,71 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
87112 $" Current version: {version}" |> output.WriteInfo
88113 Environment.Exit 0
89114
90- let handleError ( str : string ) =
115+ let handleError ( status : ExitCode ) ( str : string ) =
91116 output.WriteError str
92- exitCode <- - 1
117+ exitCode <- status
93118
94- match arguments.GetSubCommand() with
95- | Lint lintArgs ->
119+ let outputWarnings ( warnings : List < Suggestion.LintWarning >) =
120+ String.Format( Resources.GetString " ConsoleFinished" , List.length warnings)
121+ |> output.WriteInfo
122+
123+ let handleLintResult = function
124+ | LintResult.Success warnings ->
125+ outputWarnings warnings
126+ if List.isEmpty warnings |> not then
127+ exitCode <- ExitCode.Error
128+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
129+
130+ let handleFixResult ( ruleName : string ) = function
131+ | LintResult.Success warnings ->
132+ let increment = 1
133+ let noFixIncrement = 0
134+ let countSuggestedFix =
135+ warnings
136+ |> List.sumBy ( fun ( element : Suggestion.LintWarning ) ->
137+ let sourceCode = File.ReadAllText element.FilePath
138+ if String.Equals( ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
139+ match element.Details.SuggestedFix with
140+ | Some lazySuggestedFix ->
141+ lazySuggestedFix.Force()
142+ |> Option.iter ( fun suggestedFix ->
143+ String.Format( Resources.GetString " ConsoleApplyingSuggestedFixFile" , element.FilePath)
144+ |> output.WriteInfo
96145
97- let handleLintResult = function
98- | LintResult.Success( warnings) ->
99- String.Format( Resources.GetString( " ConsoleFinished" ), List.length warnings)
100- |> output.WriteInfo
101- if not ( List.isEmpty warnings) then exitCode <- - 1
102- | LintResult.Failure( failure) ->
103- handleError failure.Description
146+ let updatedSourceCode =
147+ let builder = StringBuilder( sourceCode.Length + suggestedFix.ToText.Length)
148+ let firstPart =
149+ sourceCode.AsSpan(
150+ 0 ,
151+ ( ExpressionUtilities.findPos suggestedFix.FromRange.Start sourceCode) .Value
152+ )
153+ let secondPart =
154+ sourceCode.AsSpan
155+ ( ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode) .Value
156+ builder
157+ .Append( firstPart)
158+ .Append( suggestedFix.ToText)
159+ .Append( secondPart)
160+ .ToString()
161+ File.WriteAllText(
162+ element.FilePath,
163+ updatedSourceCode,
164+ Encoding.UTF8)
165+ )
166+ increment
167+ | None -> noFixIncrement
168+ else
169+ noFixIncrement)
170+ outputWarnings warnings
104171
105- let lintConfig = lintArgs.TryGetResult Lint_ Config
172+ if countSuggestedFix > 0 then
173+ exitCode <- ExitCode.Success
174+ else
175+ exitCode <- ExitCode.NoSuggestedFix
106176
107- let configParam =
108- match lintConfig with
109- | Some configPath -> FromFile configPath
110- | None -> Default
111-
112-
113- let lintParams =
114- { CancellationToken = None
115- ReceivedWarning = Some output.WriteWarning
116- Configuration = configParam
117- ReportLinterProgress = Some ( parserProgress output) }
118-
119- let target = lintArgs.GetResult Target
120- let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
177+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
121178
179+ let linting fileType lintParams target toolsPath shouldFix maybeRuleName =
122180 try
123181 let lintResult =
124182 match fileType with
@@ -127,16 +185,70 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
127185 | FileType.Solution -> Lint.lintSolution lintParams target toolsPath
128186 | FileType.Project
129187 | _ -> Lint.lintProject lintParams target toolsPath
130- handleLintResult lintResult
188+ if shouldFix then
189+ match maybeRuleName with
190+ | Some ruleName -> handleFixResult ruleName lintResult
191+ | None -> exitCode <- ExitCode.NoSuchRuleName
192+ else
193+ handleLintResult lintResult
131194 with
132195 | e ->
133196 let target = if fileType = FileType.Source then " source" else target
134197 $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {e.Message}{Environment.NewLine}Stack trace: {e.StackTrace}"
135- |> handleError
136- | _ -> ()
198+ |> handleError ExitCode.Error
199+
200+ let getParams config =
201+ let paramConfig =
202+ match config with
203+ | Some configPath -> FromFile configPath
204+ | None -> Default
137205
138- exitCode
206+ { CancellationToken = None
207+ ReceivedWarning = Some output.WriteWarning
208+ Configuration = paramConfig
209+ ReportLinterProgress = parserProgress output |> Some }
210+
211+ let applyLint ( lintArgs : ParseResults < LintArgs >) =
212+ let lintConfig = lintArgs.TryGetResult Lint_ Config
213+
214+ let lintParams = getParams lintConfig
215+ let target = lintArgs.GetResult Target
216+ let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
217+
218+ linting fileType lintParams target toolsPath false None
219+
220+ let applySuggestedFix ( fixArgs : ParseResults < FixArgs >) =
221+ let fixParams = getParams None
222+ let ruleName , target = fixArgs.GetResult Fix_ Target
223+ let fileType = fixArgs.TryGetResult Fix_ File_ Type |> Option.defaultValue ( inferFileType target)
224+
225+ let allRules =
226+ match getConfig fixParams.Configuration with
227+ | Ok config -> Some ( Configuration.flattenConfig config false )
228+ | _ -> None
229+
230+ let allRuleNames =
231+ match allRules with
232+ | Some rules -> ( fun ( loadedRules : Configuration.LoadedRules ) -> ([|
233+ loadedRules.LineRules.IndentationRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
234+ loadedRules.LineRules.NoTabCharactersRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
235+ loadedRules.LineRules.GenericLineRules |> Array.map ( fun rule -> rule.Name)
236+ loadedRules.AstNodeRules |> Array.map ( fun rule -> rule.Name)
237+ |] |> Array.concat |> Set.ofArray)) rules
238+ | _ -> Set.empty
239+
240+ if allRuleNames.Any( fun aRuleName -> String.Equals( aRuleName, ruleName, StringComparison.InvariantCultureIgnoreCase)) then
241+ linting fileType fixParams target toolsPath true ( Some ruleName)
242+ else
243+ sprintf " Rule '%s ' does not exist." ruleName |> ( handleError ExitCode.NoSuchRuleName)
244+
245+ match arguments.GetSubCommand() with
246+ | Lint lintArgs -> applyLint lintArgs
247+ | Fix fixArgs -> applySuggestedFix fixArgs
248+ | _ -> ()
139249
250+ int exitCode
251+
140252/// Must be called only once per process.
141253/// We're calling it globally so we can call main multiple times from our tests.
142254let toolsPath = Ionide.ProjInfo.Init.init ( DirectoryInfo <| Directory.GetCurrentDirectory()) None
0 commit comments