Skip to content

Commit 01140f7

Browse files
janusparhamsaremi
authored andcommitted
Add fix command-line to apply quickfixes and unit test
1 parent 76d6e9d commit 01140f7

File tree

3 files changed

+121
-30
lines changed

3 files changed

+121
-30
lines changed

src/FSharpLint.Console/Program.fs

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
open Argu
44
open System
5+
open System.IO
6+
open System.Text
57
open FSharpLint.Framework
68
open FSharpLint.Application
79

@@ -17,17 +19,21 @@ type private FileType =
1719
| File = 3
1820
| Source = 4
1921

22+
let fileTypeHelp = "Input type the linter will run against. If this is not set, the file type will be inferred from the file extension."
23+
2024
// Allowing underscores in union case names for proper Argu command line option formatting.
2125
// fsharplint:disable UnionCasesNames
2226
type private ToolArgs =
2327
| [<AltCommandLine("-f")>] Format of OutputFormat
2428
| [<CliPrefix(CliPrefix.None)>] Lint of ParseResults<LintArgs>
29+
| [<CliPrefix(CliPrefix.None)>] Fix of ParseResults<FixArgs>
2530
with
2631
interface IArgParserTemplate with
2732
member this.Usage =
2833
match this with
2934
| Format _ -> "Output format of the linter."
3035
| Lint _ -> "Runs FSharpLint against a file or a collection of files."
36+
| Fix _ -> "Apply quickfixes for specified rule name or names (comma separated)."
3137

3238
// TODO: investigate erroneous warning on this type definition
3339
// fsharplint:disable UnionDefinitionIndentation
@@ -41,10 +47,24 @@ with
4147
member this.Usage =
4248
match this with
4349
| Target _ -> "Input to lint."
44-
| File_Type _ -> "Input type the linter will run against. If this is not set, the file type will be inferred from the file extension."
50+
| File_Type _ -> fileTypeHelp
4551
| Lint_Config _ -> "Path to the config for the lint."
4652
// fsharplint:enable UnionCasesNames
4753

54+
// TODO: investigate erroneous warning on this type definition
55+
// fsharplint:disable UnionDefinitionIndentation
56+
and private FixArgs =
57+
| [<MainCommand; Mandatory>] Fix_Target of ruleName:string * target:string
58+
| Fix_File_Type of FileType
59+
// fsharplint:enable UnionDefinitionIndentation
60+
with
61+
interface IArgParserTemplate with
62+
member this.Usage =
63+
match this with
64+
| Fix_Target _ -> "Rule name to be applied with suggestedFix and input to lint."
65+
| Fix_File_Type _ -> fileTypeHelp
66+
// fsharplint:enable UnionCasesNames
67+
4868
let private parserProgress (output:Output.IOutput) = function
4969
| Starting file ->
5070
String.Format(Resources.GetString("ConsoleStartingFile"), file) |> output.WriteInfo
@@ -83,34 +103,35 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
83103
output.WriteError str
84104
exitCode <- -1
85105

86-
match arguments.GetSubCommand() with
87-
| Lint lintArgs ->
88-
89-
let handleLintResult = function
90-
| LintResult.Success(warnings) ->
91-
String.Format(Resources.GetString("ConsoleFinished"), List.length warnings)
92-
|> output.WriteInfo
93-
if not (List.isEmpty warnings) then exitCode <- -1
94-
| LintResult.Failure(failure) ->
95-
handleError failure.Description
96-
97-
let lintConfig = lintArgs.TryGetResult Lint_Config
98-
99-
let configParam =
100-
match lintConfig with
101-
| Some configPath -> FromFile configPath
102-
| None -> Default
103-
104-
105-
let lintParams =
106-
{ CancellationToken = None
107-
ReceivedWarning = Some output.WriteWarning
108-
Configuration = configParam
109-
ReportLinterProgress = Some (parserProgress output) }
110-
111-
let target = lintArgs.GetResult Target
112-
let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target)
113-
106+
let outputWarnings (warnings: List<Suggestion.LintWarning>) =
107+
String.Format(Resources.GetString "ConsoleFinished", List.length warnings)
108+
|> output.WriteInfo
109+
110+
let handleLintResult = function
111+
| LintResult.Success warnings ->
112+
outputWarnings warnings
113+
if List.isEmpty warnings |> not then
114+
exitCode <- -1
115+
| LintResult.Failure failure -> handleError failure.Description
116+
117+
let handleFixResult (ruleName: string) = function
118+
| LintResult.Success warnings ->
119+
Resources.GetString "ConsoleApplyingSuggestedFixFile" |> output.WriteInfo
120+
List.iter (fun (element: Suggestion.LintWarning) ->
121+
let sourceCode = File.ReadAllText element.FilePath
122+
match element.Details.SuggestedFix with
123+
| Some suggestedFix when String.Equals(ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) ->
124+
suggestedFix.Force()
125+
|> Option.map (fun suggestedFix ->
126+
let updatedSourceCode = sourceCode.Replace(suggestedFix.FromText, suggestedFix.ToText)
127+
File.WriteAllText(element.FilePath, updatedSourceCode, Encoding.UTF8)) |> ignore
128+
| _ -> ()) warnings
129+
outputWarnings warnings
130+
if List.isEmpty warnings |> not then
131+
exitCode <- 0
132+
| LintResult.Failure failure -> handleError failure.Description
133+
134+
let linting fileType lintParams target toolsPath shouldFix maybeRuleName =
114135
try
115136
let lintResult =
116137
match fileType with
@@ -119,12 +140,48 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
119140
| FileType.Solution -> Lint.lintSolution lintParams target toolsPath
120141
| FileType.Project
121142
| _ -> Lint.lintProject lintParams target toolsPath
122-
handleLintResult lintResult
143+
if shouldFix then
144+
match maybeRuleName with
145+
| Some ruleName -> handleFixResult ruleName lintResult
146+
| None -> exitCode <- 1
147+
else
148+
handleLintResult lintResult
123149
with
124150
| e ->
125151
let target = if fileType = FileType.Source then "source" else target
126152
sprintf "Lint failed while analysing %s.\nFailed with: %s\nStack trace: %s" target e.Message e.StackTrace
127153
|> handleError
154+
155+
let getParams config =
156+
let paramConfig =
157+
match config with
158+
| Some configPath -> FromFile configPath
159+
| None -> Default
160+
161+
{ CancellationToken = None
162+
ReceivedWarning = Some output.WriteWarning
163+
Configuration = paramConfig
164+
ReportLinterProgress = parserProgress output |> Some }
165+
166+
let applyLint (lintArgs: ParseResults<LintArgs>) =
167+
let lintConfig = lintArgs.TryGetResult Lint_Config
168+
169+
let lintParams = getParams lintConfig
170+
let target = lintArgs.GetResult Target
171+
let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target)
172+
173+
linting fileType lintParams target toolsPath false None
174+
175+
let applySuggestedFix (fixArgs: ParseResults<FixArgs>) =
176+
let fixParams = getParams None
177+
let ruleName, target = fixArgs.GetResult Fix_Target
178+
let fileType = fixArgs.TryGetResult Fix_File_Type |> Option.defaultValue (inferFileType target)
179+
180+
linting fileType fixParams target toolsPath true (Some ruleName)
181+
182+
match arguments.GetSubCommand() with
183+
| Lint lintArgs -> applyLint lintArgs
184+
| Fix fixArgs -> applySuggestedFix fixArgs
128185
| _ -> ()
129186

130187
exitCode

src/FSharpLint.Core/Text.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@
138138
<data name="ConsoleStartingFile" xml:space="preserve">
139139
<value>========== Linting {0} ==========</value>
140140
</data>
141+
<data name="ConsoleApplyingSuggestedFixFile" xml:space="preserve">
142+
<value>========== Applying fixes ==========</value>
143+
</data>
141144
<data name="ConsoleMSBuildFailedToLoadProjectFile" xml:space="preserve">
142145
<value>MSBuild could not load the project file {0} because: {1}</value>
143146
</data>

tests/FSharpLint.Console.Tests/TestApp.fs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,34 @@ type TestConsoleApplication() =
9595

9696
Assert.AreEqual(0, returnCode)
9797
Assert.AreEqual(Set.empty, errors)
98+
99+
[<Test>]
100+
member __.``Lint source with fix option``() =
101+
let sourceCode = """
102+
module Fass =
103+
let foo = new System.Collections.Generic.Dictionary<string, string>() |> ignore
104+
let goo = new Guid() |> ignore
105+
let ntoo = new Int32() |> ignore
106+
module Fall =
107+
let uoo = new Uid() |> ignore
108+
let version = new System.Version()
109+
let xoo = new Uint32() |> ignore
110+
"""
111+
112+
let expected = """
113+
module Fass =
114+
let foo = System.Collections.Generic.Dictionary<string, string>() |> ignore
115+
let goo = Guid() |> ignore
116+
let ntoo = Int32() |> ignore
117+
module Fall =
118+
let uoo = Uid() |> ignore
119+
let version = System.Version()
120+
let xoo = Uint32() |> ignore
121+
"""
122+
let ruleName = "RedundantNewKeyword"
123+
use input = new TemporaryFile(sourceCode, "fs")
124+
let (exitCode, errors) = main [| "fix"; ruleName; input.FileName |]
125+
126+
Assert.AreEqual(0, exitCode)
127+
Assert.AreEqual(set ["Usage of `new` keyword here is redundant."], errors)
128+
Assert.AreEqual(expected, File.ReadAllText input.FileName)

0 commit comments

Comments
 (0)