Skip to content

Commit 3c64772

Browse files
janusparhamsaremiwebwarrior-ws
committed
Add fix command-line to apply quickfixes and unit test
Co-authored-by: Parham Saremi <parhaamsaremi@gmail.com> Co-authored-by: webwarrior-ws <reg@webwarrior.ws>
1 parent a583792 commit 3c64772

File tree

6 files changed

+241
-50
lines changed

6 files changed

+241
-50
lines changed

src/FSharpLint.Console/Program.fs

Lines changed: 144 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ open System.IO
66
open System.Reflection
77
open FSharpLint.Framework
88
open FSharpLint.Application
9+
open System.Linq
10+
open System.Text
911

1012
/// Output format the linter will use.
1113
type 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
2434
type 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
2839
with
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
@@ -45,10 +57,24 @@ with
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+
5278
let 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

7399
let 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,68 @@ 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+
List.fold (fun acc elem -> acc + elem) 0 (
138+
List.map (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 suggestedFix ->
143+
suggestedFix.Force()
144+
|> Option.map (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+
builder.Append firstPart |> ignore
153+
builder.Append suggestedFix.ToText |> ignore
154+
let secondPart =
155+
let index = (ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode).Value
156+
sourceCode.AsSpan(index, sourceCode.Length - index)
157+
builder.Append secondPart |> ignore
158+
builder.ToString()
159+
File.WriteAllText(
160+
element.FilePath,
161+
updatedSourceCode,
162+
Encoding.UTF8)
163+
)
164+
|> ignore |> fun () -> increment
165+
| None -> noFixIncrement
166+
else
167+
noFixIncrement) warnings)
168+
outputWarnings warnings
96169

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
170+
if countSuggestedFix > 0 then
171+
exitCode <- ExitCode.Success
172+
else
173+
exitCode <- ExitCode.NoSuggestedFix
104174

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)
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
134-
$"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{e.Message}{Environment.NewLine}Stack trace: {e.StackTrace}"
135-
|> handleError
136-
| _ -> ()
195+
sprintf "Lint failed while analysing %s.\nFailed with: %s\nStack trace: %s" target e.Message e.StackTrace
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.
142252
let toolsPath = Ionide.ProjInfo.Init.init (DirectoryInfo <| Directory.GetCurrentDirectory()) None

src/FSharpLint.Core/Application/Configuration.fs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,20 @@ type RuleConfig<'Config> = {
103103

104104
type EnabledConfig = RuleConfig<unit>
105105

106-
let constructRuleIfEnabled rule ruleConfig = if ruleConfig.Enabled then Some rule else None
106+
let constructRuleIfEnabledBase (onlyEnabled: bool) rule ruleConfig =
107+
if not onlyEnabled || ruleConfig.Enabled then Some rule else None
107108

108-
let constructRuleWithConfig rule ruleConfig =
109-
if ruleConfig.Enabled then
110-
ruleConfig.Config |> Option.map rule
111-
else
112-
None
109+
let constructRuleIfEnabled rule ruleConfig =
110+
constructRuleIfEnabledBase true rule ruleConfig
111+
112+
let constructRuleWithConfigBase (onlyEnabled: bool) (rule: 'TRuleConfig -> 'TRule) (ruleConfig: RuleConfig<'TRuleConfig>): Option<'TRule> =
113+
if not onlyEnabled || ruleConfig.Enabled then
114+
ruleConfig.Config |> Option.map rule
115+
else
116+
None
117+
118+
let constructRuleWithConfig (rule: 'TRuleConfig -> 'TRule) (ruleConfig: RuleConfig<'TRuleConfig>): Option<'TRule> =
119+
constructRuleWithConfigBase true rule ruleConfig
113120

114121
let constructTypePrefixingRuleWithConfig rule (ruleConfig: RuleConfig<TypePrefixing.Config>) =
115122
if ruleConfig.Enabled then
@@ -617,7 +624,7 @@ let private parseHints (hints:string []) =
617624
|> Array.toList
618625
|> MergeSyntaxTrees.mergeHints
619626

620-
let flattenConfig (config:Configuration) =
627+
let flattenConfig (config:Configuration) (onlyEnabled:bool) =
621628
let deprecatedAllRules =
622629
[|
623630
config.formatting |> Option.map (fun config -> config.Flatten()) |> Option.toArray |> Array.concat
@@ -626,6 +633,12 @@ let flattenConfig (config:Configuration) =
626633
config.Hints |> Option.map (fun config -> HintMatcher.rule { HintMatcher.Config.HintTrie = parseHints (getOrEmptyList config.add) }) |> Option.toArray
627634
|] |> Array.concat
628635

636+
let constructRuleIfEnabled rule ruleConfig =
637+
constructRuleIfEnabledBase onlyEnabled rule ruleConfig
638+
639+
let constructRuleWithConfig (rule: 'TRuleConfig -> 'TRule) (ruleConfig: RuleConfig<'TRuleConfig>): Option<'TRule> =
640+
constructRuleWithConfigBase onlyEnabled rule ruleConfig
641+
629642
let allRules =
630643
[|
631644
config.TypedItemSpacing |> Option.bind (constructRuleWithConfig TypedItemSpacing.rule)

src/FSharpLint.Core/Application/Lint.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ module Lint =
207207
| Some(x) -> not x.IsCancellationRequested
208208
| None -> true
209209

210-
let enabledRules = Configuration.flattenConfig lintInfo.Configuration
210+
let enabledRules = Configuration.flattenConfig lintInfo.Configuration true
211211

212212
let lines = String.toLines fileInfo.Text |> Array.map (fun (line, _, _) -> line)
213213
let allRuleNames =
@@ -373,7 +373,7 @@ module Lint =
373373
}
374374

375375
/// Gets a FSharpLint Configuration based on the provided ConfigurationParam.
376-
let private getConfig (configParam:ConfigurationParam) =
376+
let getConfig (configParam:ConfigurationParam) =
377377
match configParam with
378378
| Configuration config -> Ok config
379379
| FromFile filePath ->

src/FSharpLint.Core/Application/Lint.fsi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,6 @@ module Lint =
144144

145145
/// Lints an F# file that has already been parsed using
146146
/// `FSharp.Compiler.Services` in the calling application.
147-
val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult
147+
val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult
148+
149+
val getConfig : ConfigurationParam -> Result<Configuration,string>

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>

0 commit comments

Comments
 (0)