@@ -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,69 @@ 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
93-
94- match arguments.GetSubCommand() with
95- | Lint lintArgs ->
96-
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
104-
105- let lintConfig = lintArgs.TryGetResult Lint_ Config
106-
107- let configParam =
108- match lintConfig with
109- | Some configPath -> FromFile configPath
110- | None -> Default
117+ exitCode <- status
111118
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+ String.Format( Resources.GetString " ConsoleApplyingSuggestedFixFile" , ruleName) |> output.WriteInfo
133+ let increment = 1
134+ let noFixIncrement = 0
135+ let countSuggestedFix =
136+ warnings
137+ |> List.sumBy ( fun ( element : Suggestion.LintWarning ) ->
138+ let sourceCode = File.ReadAllText element.FilePath
139+ if String.Equals( ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
140+ match element.Details.SuggestedFix with
141+ | Some lazySuggestedFix ->
142+ lazySuggestedFix.Force()
143+ |> Option.iter ( fun suggestedFix ->
144+ let updatedSourceCode =
145+ let builder = StringBuilder( sourceCode.Length + suggestedFix.ToText.Length)
146+ let firstPart =
147+ sourceCode.AsSpan(
148+ 0 ,
149+ ( ExpressionUtilities.findPos suggestedFix.FromRange.Start sourceCode) .Value
150+ )
151+ let secondPart =
152+ sourceCode.AsSpan
153+ ( ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode) .Value
154+ builder
155+ .Append( firstPart)
156+ .Append( suggestedFix.ToText)
157+ .Append( secondPart)
158+ .ToString()
159+ File.WriteAllText(
160+ element.FilePath,
161+ updatedSourceCode,
162+ Encoding.UTF8)
163+ )
164+ increment
165+ | None -> noFixIncrement
166+ else
167+ noFixIncrement)
168+ outputWarnings warnings
112169
113- let lintParams =
114- { CancellationToken = None
115- ReceivedWarning = Some output.WriteWarning
116- Configuration = configParam
117- ReportLinterProgress = Some ( parserProgress output) }
170+ if countSuggestedFix > 0 then
171+ exitCode <- ExitCode.Success
172+ else
173+ exitCode <- ExitCode.NoSuggestedFix
118174
119- let target = lintArgs.GetResult Target
120- let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
175+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
121176
177+ let linting fileType lintParams target toolsPath shouldFix maybeRuleName =
122178 try
123179 let lintResult =
124180 match fileType with
@@ -127,16 +183,70 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
127183 | FileType.Solution -> Lint.lintSolution lintParams target toolsPath
128184 | FileType.Project
129185 | _ -> Lint.lintProject lintParams target toolsPath
130- handleLintResult lintResult
186+ if shouldFix then
187+ match maybeRuleName with
188+ | Some ruleName -> handleFixResult ruleName lintResult
189+ | None -> exitCode <- ExitCode.NoSuchRuleName
190+ else
191+ handleLintResult lintResult
131192 with
132193 | e ->
133194 let target = if fileType = FileType.Source then " source" else target
134195 $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {e.Message}{Environment.NewLine}Stack trace: {e.StackTrace}"
135- |> handleError
136- | _ -> ()
196+ |> handleError ExitCode.Error
197+
198+ let getParams config =
199+ let paramConfig =
200+ match config with
201+ | Some configPath -> FromFile configPath
202+ | None -> Default
203+
204+ { CancellationToken = None
205+ ReceivedWarning = Some output.WriteWarning
206+ Configuration = paramConfig
207+ ReportLinterProgress = parserProgress output |> Some }
208+
209+ let applyLint ( lintArgs : ParseResults < LintArgs >) =
210+ let lintConfig = lintArgs.TryGetResult Lint_ Config
137211
138- exitCode
212+ let lintParams = getParams lintConfig
213+ let target = lintArgs.GetResult Target
214+ let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
215+
216+ linting fileType lintParams target toolsPath false None
217+
218+ let applySuggestedFix ( fixArgs : ParseResults < FixArgs >) =
219+ let fixParams = getParams None
220+ let ruleName , target = fixArgs.GetResult Fix_ Target
221+ let fileType = fixArgs.TryGetResult Fix_ File_ Type |> Option.defaultValue ( inferFileType target)
222+
223+ let allRules =
224+ match getConfig fixParams.Configuration with
225+ | Ok config -> Some ( Configuration.flattenConfig config false )
226+ | _ -> None
227+
228+ let allRuleNames =
229+ match allRules with
230+ | Some rules -> ( fun ( loadedRules : Configuration.LoadedRules ) -> ([|
231+ loadedRules.LineRules.IndentationRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
232+ loadedRules.LineRules.NoTabCharactersRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
233+ loadedRules.LineRules.GenericLineRules |> Array.map ( fun rule -> rule.Name)
234+ loadedRules.AstNodeRules |> Array.map ( fun rule -> rule.Name)
235+ |] |> Array.concat |> Set.ofArray)) rules
236+ | _ -> Set.empty
237+
238+ if allRuleNames.Any( fun aRuleName -> String.Equals( aRuleName, ruleName, StringComparison.InvariantCultureIgnoreCase)) then
239+ linting fileType fixParams target toolsPath true ( Some ruleName)
240+ else
241+ sprintf " Rule '%s ' does not exist." ruleName |> ( handleError ExitCode.NoSuchRuleName)
242+
243+ match arguments.GetSubCommand() with
244+ | Lint lintArgs -> applyLint lintArgs
245+ | Fix fixArgs -> applySuggestedFix fixArgs
246+ | _ -> ()
139247
248+ int exitCode
249+
140250/// Must be called only once per process.
141251/// We're calling it globally so we can call main multiple times from our tests.
142252let toolsPath = Ionide.ProjInfo.Init.init ( DirectoryInfo <| Directory.GetCurrentDirectory()) None
0 commit comments