@@ -6,6 +6,8 @@ open System.IO
66open System.Reflection
77open FSharpLint.Framework
88open FSharpLint.Application
9+ open System.Linq
10+ open System.Text
911
1012/// Output format the linter will use.
1113type private OutputFormat =
@@ -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."
5062// fsharplint:enable UnionCasesNames
5163
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
76+ // fsharplint:enable UnionCasesNames
77+
5278let private parserProgress ( output : Output.IOutput ) = function
5379 | Starting file ->
5480 String.Format( Resources.GetString( " ConsoleStartingFile" ), file) |> output.WriteInfo
@@ -71,7 +97,7 @@ let internal inferFileType (target:string) =
7197 FileType.Source
7298
7399let private start ( arguments : ParseResults < ToolArgs >) ( toolsPath : Ionide.ProjInfo.Types.ToolsPath ) =
74- let mutable exitCode = 0
100+ let mutable exitCode = ExitCode.Success
75101
76102 let output =
77103 match arguments.TryGetResult Format with
@@ -87,38 +113,69 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
87113 $" Current version: {version}" |> output.WriteInfo
88114 Environment.Exit 0
89115
90- let handleError ( str : string ) =
116+ let handleError ( status : ExitCode ) ( str : string ) =
91117 output.WriteError str
92- exitCode <- - 1
118+ exitCode <- status
93119
94- match arguments.GetSubCommand() with
95- | Lint lintArgs ->
120+ let outputWarnings ( warnings : List < Suggestion.LintWarning >) =
121+ String.Format( Resources.GetString " ConsoleFinished" , List.length warnings)
122+ |> output.WriteInfo
123+
124+ let handleLintResult = function
125+ | LintResult.Success warnings ->
126+ outputWarnings warnings
127+ if List.isEmpty warnings |> not then
128+ exitCode <- ExitCode.Error
129+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
130+
131+ let handleFixResult ( ruleName : string ) = function
132+ | LintResult.Success warnings ->
133+ Resources.GetString " ConsoleApplyingSuggestedFixFile" |> output.WriteInfo
134+ let increment = 1
135+ let noFixIncrement = 0
136+ let countSuggestedFix =
137+ warnings
138+ |> List.sumBy ( fun ( element : Suggestion.LintWarning ) ->
139+ let sourceCode = File.ReadAllText element.FilePath
140+ if String.Equals( ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
141+ match element.Details.SuggestedFix with
142+ | Some lazySuggestedFix ->
143+ lazySuggestedFix.Force()
144+ |> Option.iter ( fun suggestedFix ->
145+ let updatedSourceCode =
146+ let builder = StringBuilder( sourceCode.Length + suggestedFix.ToText.Length)
147+ let firstPart =
148+ sourceCode.AsSpan(
149+ 0 ,
150+ ( ExpressionUtilities.findPos suggestedFix.FromRange.Start sourceCode) .Value
151+ )
152+ let secondPart =
153+ sourceCode.AsSpan
154+ ( ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode) .Value
155+ builder
156+ .Append( firstPart)
157+ .Append( suggestedFix.ToText)
158+ .Append( secondPart)
159+ .ToString()
160+ File.WriteAllText(
161+ element.FilePath,
162+ updatedSourceCode,
163+ Encoding.UTF8)
164+ )
165+ increment
166+ | None -> noFixIncrement
167+ else
168+ noFixIncrement)
169+ outputWarnings warnings
96170
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
171+ if countSuggestedFix > 0 then
172+ exitCode <- ExitCode.Success
173+ else
174+ exitCode <- ExitCode.NoSuggestedFix
104175
105- let lintConfig = lintArgs.TryGetResult Lint_ Config
106-
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)
176+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
121177
178+ let linting fileType lintParams target toolsPath shouldFix maybeRuleName =
122179 try
123180 let lintResult =
124181 match fileType with
@@ -127,16 +184,70 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
127184 | FileType.Solution -> Lint.lintSolution lintParams target toolsPath
128185 | FileType.Project
129186 | _ -> Lint.lintProject lintParams target toolsPath
130- handleLintResult lintResult
187+ if shouldFix then
188+ match maybeRuleName with
189+ | Some ruleName -> handleFixResult ruleName lintResult
190+ | None -> exitCode <- ExitCode.NoSuchRuleName
191+ else
192+ handleLintResult lintResult
131193 with
132194 | e ->
133195 let target = if fileType = FileType.Source then " source" else target
134- $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {e.Message}{Environment.NewLine}Stack trace: {e.StackTrace}"
135- |> handleError
136- | _ -> ()
196+ sprintf " Lint failed while analysing %s .\n Failed with: %s \n Stack trace: %s " target e.Message e.StackTrace
197+ |> ( handleError ExitCode.Error)
198+
199+ let getParams config =
200+ let paramConfig =
201+ match config with
202+ | Some configPath -> FromFile configPath
203+ | None -> Default
204+
205+ { CancellationToken = None
206+ ReceivedWarning = Some output.WriteWarning
207+ Configuration = paramConfig
208+ ReportLinterProgress = parserProgress output |> Some }
209+
210+ let applyLint ( lintArgs : ParseResults < LintArgs >) =
211+ let lintConfig = lintArgs.TryGetResult Lint_ Config
137212
138- exitCode
213+ let lintParams = getParams lintConfig
214+ let target = lintArgs.GetResult Target
215+ let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
216+
217+ linting fileType lintParams target toolsPath false None
218+
219+ let applySuggestedFix ( fixArgs : ParseResults < FixArgs >) =
220+ let fixParams = getParams None
221+ let ruleName , target = fixArgs.GetResult Fix_ Target
222+ let fileType = fixArgs.TryGetResult Fix_ File_ Type |> Option.defaultValue ( inferFileType target)
223+
224+ let allRules =
225+ match getConfig fixParams.Configuration with
226+ | Ok config -> Some ( Configuration.flattenConfig config false )
227+ | _ -> None
228+
229+ let allRuleNames =
230+ match allRules with
231+ | Some rules -> ( fun ( loadedRules : Configuration.LoadedRules ) -> ([|
232+ loadedRules.LineRules.IndentationRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
233+ loadedRules.LineRules.NoTabCharactersRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
234+ loadedRules.LineRules.GenericLineRules |> Array.map ( fun rule -> rule.Name)
235+ loadedRules.AstNodeRules |> Array.map ( fun rule -> rule.Name)
236+ |] |> Array.concat |> Set.ofArray)) rules
237+ | _ -> Set.empty
238+
239+ if allRuleNames.Any( fun aRuleName -> String.Equals( aRuleName, ruleName, StringComparison.InvariantCultureIgnoreCase)) then
240+ linting fileType fixParams target toolsPath true ( Some ruleName)
241+ else
242+ sprintf " Rule '%s ' does not exist." ruleName |> ( handleError ExitCode.NoSuchRuleName)
243+
244+ match arguments.GetSubCommand() with
245+ | Lint lintArgs -> applyLint lintArgs
246+ | Fix fixArgs -> applySuggestedFix fixArgs
247+ | _ -> ()
139248
249+ int exitCode
250+
140251/// Must be called only once per process.
141252/// We're calling it globally so we can call main multiple times from our tests.
142253let toolsPath = Ionide.ProjInfo.Init.init ( DirectoryInfo <| Directory.GetCurrentDirectory()) None
0 commit comments