@@ -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
77+ type private LintingArgs =
78+ {
79+ FileType: FileType
80+ LintParams: OptionalLintParameters
81+ Target: string
82+ ToolsPath: Ionide .ProjInfo .Types .ToolsPath
83+ ShouldFix: bool
84+ MaybeRuleName: string option
85+ }
86+
5287let private parserProgress ( output : Output.IOutput ) = function
5388 | Starting file ->
5489 String.Format( Resources.GetString( " ConsoleStartingFile" ), file) |> output.WriteInfo
@@ -70,8 +105,10 @@ let internal inferFileType (target:string) =
70105 else
71106 FileType.Source
72107
108+ // can't extract inner functions because they modify exitCode variable
109+ // fsharplint:disable MaxLinesInFunction
73110let private start ( arguments : ParseResults < ToolArgs >) ( toolsPath : Ionide.ProjInfo.Types.ToolsPath ) =
74- let mutable exitCode = 0
111+ let mutable exitCode = ExitCode.Success
75112
76113 let output =
77114 match arguments.TryGetResult Format with
@@ -87,56 +124,156 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
87124 output.WriteInfo $" Current version: {version}"
88125 Environment.Exit 0
89126
90- let handleError ( str : string ) =
127+ let handleError ( status : ExitCode ) ( str : string ) =
91128 output.WriteError str
92- exitCode <- - 1
129+ exitCode <- status
93130
94- match arguments.GetSubCommand() with
95- | Lint lintArgs ->
131+ let outputWarnings ( warnings : List < Suggestion.LintWarning >) =
132+ String.Format( Resources.GetString " ConsoleFinished" , List.length warnings)
133+ |> output.WriteInfo
134+
135+ let handleLintResult = function
136+ | LintResult.Success warnings ->
137+ outputWarnings warnings
138+ if List.isEmpty warnings |> not then
139+ exitCode <- ExitCode.Error
140+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
141+
142+ let handleFixResult ( target : string ) ( ruleName : string ) = function
143+ | LintResult.Success warnings ->
144+ String.Format( Resources.GetString " ConsoleApplyingSuggestedFixFile" , target) |> output.WriteInfo
145+ let increment = 1
146+ let noFixIncrement = 0
96147
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
148+ let countFixes ( element : Suggestion.LintWarning ) =
149+ let sourceCode = File.ReadAllText element.FilePath
150+ if String.Equals( ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
151+ match element.Details.SuggestedFix with
152+ | Some lazySuggestedFix ->
153+ match lazySuggestedFix.Force() with
154+ | Some suggestedFix ->
155+ let updatedSourceCode =
156+ let builder = StringBuilder( sourceCode.Length + suggestedFix.ToText.Length)
157+ let firstPart =
158+ sourceCode.AsSpan(
159+ 0 ,
160+ ( ExpressionUtilities.findPos suggestedFix.FromRange.Start sourceCode) .Value
161+ )
162+ let secondPart =
163+ sourceCode.AsSpan
164+ ( ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode) .Value
165+ builder
166+ .Append( firstPart)
167+ .Append( suggestedFix.ToText)
168+ .Append( secondPart)
169+ .ToString()
170+ File.WriteAllText(
171+ element.FilePath,
172+ updatedSourceCode,
173+ Encoding.UTF8)
174+ | _ -> ()
175+ increment
176+ | None -> noFixIncrement
177+ else
178+ noFixIncrement
104179
105- let lintConfig = lintArgs.TryGetResult Lint_ Config
180+ let countSuggestedFix =
181+ warnings |> List.sumBy countFixes
182+ outputWarnings warnings
106183
107- let configParam =
108- match lintConfig with
184+ if countSuggestedFix > 0 then
185+ exitCode <- ExitCode.Success
186+ else
187+ exitCode <- ExitCode.NoSuggestedFix
188+
189+ | LintResult.Failure failure -> handleError ExitCode.Error failure.Description
190+
191+ let linting ( args : LintingArgs ) =
192+ try
193+ let lintResult =
194+ match args.FileType with
195+ | FileType.File -> Lint.lintFile args.LintParams args.Target
196+ | FileType.Source -> Lint.lintSource args.LintParams args.Target
197+ | FileType.Solution -> Lint.lintSolution args.LintParams args.Target toolsPath
198+ | FileType.Project
199+ | _ -> Lint.lintProject args.LintParams args.Target toolsPath
200+ if args.ShouldFix then
201+ match args.MaybeRuleName with
202+ | Some ruleName -> handleFixResult args.Target ruleName lintResult
203+ | None -> exitCode <- ExitCode.NoSuchRuleName
204+ else
205+ handleLintResult lintResult
206+ with
207+ | exn ->
208+ let target = if args.FileType = FileType.Source then " source" else args.Target
209+ $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
210+ |> handleError ExitCode.Error
211+
212+ let getParams config =
213+ let paramConfig =
214+ match config with
109215 | Some configPath -> FromFile configPath
110216 | None -> Default
111217
218+ { CancellationToken = None
219+ ReceivedWarning = Some output.WriteWarning
220+ Configuration = paramConfig
221+ ReportLinterProgress = parserProgress output |> Some }
112222
113- let lintParams =
114- { CancellationToken = None
115- ReceivedWarning = Some output.WriteWarning
116- Configuration = configParam
117- ReportLinterProgress = Some ( parserProgress output) }
223+ let applyLint ( lintArgs : ParseResults < LintArgs >) =
224+ let lintConfig = lintArgs.TryGetResult Lint_ Config
118225
226+ let lintParams = getParams lintConfig
119227 let target = lintArgs.GetResult Target
120228 let fileType = lintArgs.TryGetResult File_ Type |> Option.defaultValue ( inferFileType target)
121229
122- try
123- let lintResult =
124- match fileType with
125- | FileType.File -> Lint.lintFile lintParams target
126- | FileType.Source -> Lint.lintSource lintParams target
127- | FileType.Solution -> Lint.lintSolution lintParams target toolsPath
128- | FileType.Project
129- | _ -> Lint.lintProject lintParams target toolsPath
130- handleLintResult lintResult
131- with
132- | exn ->
133- let target = if fileType = FileType.Source then " source" else target
134- handleError
135- $" Lint failed while analysing %s {target}.{Environment.NewLine}Failed with: %s {exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
136- | _ -> ()
230+ linting
231+ { FileType = fileType
232+ LintParams = lintParams
233+ Target = target
234+ ToolsPath = toolsPath
235+ ShouldFix = false
236+ MaybeRuleName = None }
237+
238+ let applySuggestedFix ( fixArgs : ParseResults < FixArgs >) =
239+ let fixParams = getParams None
240+ let ruleName , target = fixArgs.GetResult Fix_ Target
241+ let fileType = fixArgs.TryGetResult Fix_ File_ Type |> Option.defaultValue ( inferFileType target)
242+
243+ let allRules =
244+ match getConfig fixParams.Configuration with
245+ | Ok config -> Some ( Configuration.flattenConfig config false )
246+ | _ -> None
137247
138- exitCode
248+ let allRuleNames =
249+ match allRules with
250+ | Some rules -> ( fun ( loadedRules : Configuration.LoadedRules ) -> ([|
251+ loadedRules.LineRules.IndentationRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
252+ loadedRules.LineRules.NoTabCharactersRule |> Option.map ( fun rule -> rule.Name) |> Option.toArray
253+ loadedRules.LineRules.GenericLineRules |> Array.map ( fun rule -> rule.Name)
254+ loadedRules.AstNodeRules |> Array.map ( fun rule -> rule.Name)
255+ |] |> Array.concat |> Set.ofArray)) rules
256+ | _ -> Set.empty
257+
258+ if allRuleNames.Any( fun aRuleName -> String.Equals( aRuleName, ruleName, StringComparison.InvariantCultureIgnoreCase)) then
259+ linting
260+ { FileType = fileType
261+ LintParams = fixParams
262+ Target = target
263+ ToolsPath = toolsPath
264+ ShouldFix = true
265+ MaybeRuleName = Some ruleName }
266+ else
267+ sprintf " Rule '%s ' does not exist." ruleName |> ( handleError ExitCode.NoSuchRuleName)
268+
269+ match arguments.GetSubCommand() with
270+ | Lint lintArgs -> applyLint lintArgs
271+ | Fix fixArgs -> applySuggestedFix fixArgs
272+ | _ -> ()
139273
274+ int exitCode
275+ // fsharplint:enable MaxLinesInFunction
276+
140277/// Must be called only once per process.
141278/// We're calling it globally so we can call main multiple times from our tests.
142279let toolsPath = Ionide.ProjInfo.Init.init ( DirectoryInfo <| Directory.GetCurrentDirectory()) None
0 commit comments