Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 162 additions & 33 deletions src/FSharpLint.Console/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

open Argu
open System
open System.IO
open System.Text
open FSharpLint.Framework
open FSharpLint.Application
open System.Linq

/// Output format the linter will use.
type private OutputFormat =
Expand All @@ -17,17 +20,31 @@ type private FileType =
| File = 3
| Source = 4

type ExitCode =
| Error = -1
// for `fix` when the file is changed and for `fix --check` when there are no fixes available.
| Success = 0
| NoSuchRuleName = 1
// only for fix (without --check)
| NoSuggestedFix = 2
//only for fix --check
| FixExists = 3

let fileTypeHelp = "Input type the linter will run against. If this is not set, the file type will be inferred from the file extension."

// Allowing underscores in union case names for proper Argu command line option formatting.
// fsharplint:disable UnionCasesNames
type private ToolArgs =
| [<AltCommandLine("-f")>] Format of OutputFormat
| [<CliPrefix(CliPrefix.None)>] Lint of ParseResults<LintArgs>
| [<CliPrefix(CliPrefix.None)>] Fix of ParseResults<FixArgs>
with
interface IArgParserTemplate with
member this.Usage =
match this with
| Format _ -> "Output format of the linter."
| Lint _ -> "Runs FSharpLint against a file or a collection of files."
| Fix _ -> "Apply quickfixes for specified rule name or names (comma separated)."

// TODO: investigate erroneous warning on this type definition
// fsharplint:disable UnionDefinitionIndentation
Expand All @@ -41,10 +58,26 @@ with
member this.Usage =
match this with
| Target _ -> "Input to lint."
| File_Type _ -> "Input type the linter will run against. If this is not set, the file type will be inferred from the file extension."
| File_Type _ -> fileTypeHelp
| Lint_Config _ -> "Path to the config for the lint."
// fsharplint:enable UnionCasesNames

// TODO: investigate erroneous warning on this type definition
// fsharplint:disable UnionDefinitionIndentation
and private FixArgs =
| [<MainCommand; Mandatory>] Fix_Target of ruleName:string * target:string
| Fix_File_Type of FileType
| Check
// fsharplint:enable UnionDefinitionIndentation
with
interface IArgParserTemplate with
member this.Usage =
match this with
| Fix_Target _ -> "Rule name to be applied with suggestedFix and input to lint."
| Fix_File_Type _ -> fileTypeHelp
| Check _ -> "If passed to the fix command, the linter will only check if the fix is needed."
// fsharplint:enable UnionCasesNames

let private parserProgress (output:Output.IOutput) = function
| Starting file ->
String.Format(Resources.GetString("ConsoleStartingFile"), file) |> output.WriteInfo
Expand All @@ -70,7 +103,7 @@ let private inferFileType (target:string) =
FileType.Source

let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) =
let mutable exitCode = 0
let mutable exitCode = ExitCode.Success

let output =
match arguments.TryGetResult Format with
Expand All @@ -79,38 +112,79 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
| Some _
| None -> Output.StandardOutput() :> Output.IOutput

let handleError (str:string) =
let handleError (status: ExitCode) (str: string) =
output.WriteError str
exitCode <- -1

match arguments.GetSubCommand() with
| Lint lintArgs ->

let handleLintResult = function
| LintResult.Success(warnings) ->
String.Format(Resources.GetString("ConsoleFinished"), List.length warnings)
|> output.WriteInfo
if not (List.isEmpty warnings) then exitCode <- -1
| LintResult.Failure(failure) ->
handleError failure.Description

let lintConfig = lintArgs.TryGetResult Lint_Config

let configParam =
match lintConfig with
| Some configPath -> FromFile configPath
| None -> Default

exitCode <- status

let lintParams =
{ CancellationToken = None
ReceivedWarning = Some output.WriteWarning
Configuration = configParam
ReportLinterProgress = Some (parserProgress output) }
let outputWarnings (warnings: List<Suggestion.LintWarning>) =
String.Format(Resources.GetString "ConsoleFinished", List.length warnings)
|> output.WriteInfo

let handleLintResult = function
| LintResult.Success warnings ->
outputWarnings warnings
if List.isEmpty warnings |> not then
exitCode <- ExitCode.Error
| LintResult.Failure failure -> handleError ExitCode.Error failure.Description

let handleFixResult (ruleName: string) (checkFlag: bool) = function
| LintResult.Success warnings ->
if not checkFlag then
Resources.GetString "ConsoleApplyingSuggestedFixFile" |> output.WriteInfo
let noFixIncrement = 0
let foundFixIncrement = 1
let noSuggestedFixIncrement = 2
let countFixStatus =
List.fold (fun (accNoFix, accFoundFix, accNoSuggestedFix) elem ->
if elem = noFixIncrement then
(accNoFix + 1, accFoundFix, accNoSuggestedFix)
elif elem = foundFixIncrement then
(accNoFix, accFoundFix + 1, accNoSuggestedFix)
elif elem = noSuggestedFixIncrement then
(accNoFix, accFoundFix, accNoSuggestedFix + 1)
else
failwith "Code should never reach here!") (0, 0, 0) (
List.map (fun (element: Suggestion.LintWarning) ->
let sourceCode = File.ReadAllText element.FilePath
if String.Equals(ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
match element.Details.SuggestedFix with
| Some suggestedFix ->
((fun checkFlag ->
if not checkFlag then
(suggestedFix.Force()
|> Option.map (fun suggestedFix ->
let updatedSourceCode =
sourceCode.Replace(
suggestedFix.FromText,
suggestedFix.ToText
)
File.WriteAllText(
element.FilePath,
updatedSourceCode,
Encoding.UTF8)
)
) |> ignore
else
()
) checkFlag)
|> ignore |> fun () -> foundFixIncrement
| None -> noSuggestedFixIncrement
else
noFixIncrement) warnings)
outputWarnings warnings
let (accNoFix, accFoundFix, accNoSuggestedFix) = countFixStatus
if not checkFlag && accFoundFix > 0 then
exitCode <- ExitCode.Success
elif not checkFlag then
exitCode <- ExitCode.NoSuggestedFix
elif checkFlag && accFoundFix > 0 then
exitCode <- ExitCode.FixExists
elif checkFlag then
exitCode <- ExitCode.Success

let target = lintArgs.GetResult Target
let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target)
| LintResult.Failure failure -> handleError ExitCode.Error failure.Description

let linting fileType lintParams target toolsPath shouldFix maybeRuleName checkFlag =
try
let lintResult =
match fileType with
Expand All @@ -119,15 +193,70 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
| FileType.Solution -> Lint.lintSolution lintParams target toolsPath
| FileType.Project
| _ -> Lint.lintProject lintParams target toolsPath
handleLintResult lintResult
if shouldFix then
match maybeRuleName with
| Some ruleName -> handleFixResult ruleName checkFlag lintResult
| None -> exitCode <- ExitCode.NoSuchRuleName
else
handleLintResult lintResult
with
| e ->
let target = if fileType = FileType.Source then "source" else target
sprintf "Lint failed while analysing %s.\nFailed with: %s\nStack trace: %s" target e.Message e.StackTrace
|> handleError
|> (handleError ExitCode.Error)

let getParams config =
let paramConfig =
match config with
| Some configPath -> FromFile configPath
| None -> Default

{ CancellationToken = None
ReceivedWarning = Some output.WriteWarning
Configuration = paramConfig
ReportLinterProgress = parserProgress output |> Some }

let applyLint (lintArgs: ParseResults<LintArgs>) =
let lintConfig = lintArgs.TryGetResult Lint_Config

let lintParams = getParams lintConfig
let target = lintArgs.GetResult Target
let fileType = lintArgs.TryGetResult File_Type |> Option.defaultValue (inferFileType target)

linting fileType lintParams target toolsPath false None false

let applySuggestedFix (fixArgs: ParseResults<FixArgs>) =
let fixParams = getParams None
let ruleName, target = fixArgs.GetResult Fix_Target
let fileType = fixArgs.TryGetResult Fix_File_Type |> Option.defaultValue (inferFileType target)
let checkFlag = fixArgs.Contains Check

let allRules =
match getConfig fixParams.Configuration with
| Ok config -> Some (Configuration.flattenConfig config false)
| _ -> None

let allRuleNames =
match allRules with
| Some rules -> (fun (loadedRules:Configuration.LoadedRules) -> ([|
loadedRules.LineRules.IndentationRule |> Option.map (fun rule -> rule.Name) |> Option.toArray
loadedRules.LineRules.NoTabCharactersRule |> Option.map (fun rule -> rule.Name) |> Option.toArray
loadedRules.LineRules.GenericLineRules |> Array.map (fun rule -> rule.Name)
loadedRules.AstNodeRules |> Array.map (fun rule -> rule.Name)
|] |> Array.concat |> Set.ofArray)) rules
| _ -> Set.empty

if allRuleNames.Any(fun aRuleName -> String.Equals(aRuleName, ruleName, StringComparison.InvariantCultureIgnoreCase)) then
linting fileType fixParams target toolsPath true (Some ruleName) checkFlag
else
sprintf "Rule '%s' does not exist." ruleName |> (handleError ExitCode.NoSuchRuleName)

match arguments.GetSubCommand() with
| Lint lintArgs -> applyLint lintArgs
| Fix fixArgs -> applySuggestedFix fixArgs
| _ -> ()

exitCode
int exitCode

/// Must be called only once per process.
/// We're calling it globally so we can call main multiple times from our tests.
Expand Down
27 changes: 20 additions & 7 deletions src/FSharpLint.Core/Application/Configuration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,20 @@ type RuleConfig<'Config> = {

type EnabledConfig = RuleConfig<unit>

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

let constructRuleWithConfig rule ruleConfig =
if ruleConfig.Enabled then
ruleConfig.Config |> Option.map rule
else
None
let constructRuleIfEnabled rule ruleConfig =
constructRuleIfEnabledBase true rule ruleConfig

let constructRuleWithConfigBase (onlyEnabled: bool) (rule: 'TRuleConfig -> 'TRule) (ruleConfig: RuleConfig<'TRuleConfig>): Option<'TRule> =
if not onlyEnabled || ruleConfig.Enabled then
ruleConfig.Config |> Option.map rule
else
None

let constructRuleWithConfig (rule: 'TRuleConfig -> 'TRule) (ruleConfig: RuleConfig<'TRuleConfig>): Option<'TRule> =
constructRuleWithConfigBase true rule ruleConfig

type TupleFormattingConfig =
{ tupleCommaSpacing:EnabledConfig option
Expand Down Expand Up @@ -600,7 +607,7 @@ let private parseHints (hints:string []) =
|> Array.toList
|> MergeSyntaxTrees.mergeHints

let flattenConfig (config:Configuration) =
let flattenConfig (config:Configuration) (onlyEnabled:bool) =
let deprecatedAllRules =
[|
config.formatting |> Option.map (fun config -> config.Flatten()) |> Option.toArray |> Array.concat
Expand All @@ -609,6 +616,12 @@ let flattenConfig (config:Configuration) =
config.Hints |> Option.map (fun config -> HintMatcher.rule { HintMatcher.Config.HintTrie = parseHints (getOrEmptyList config.add) }) |> Option.toArray
|] |> Array.concat

let constructRuleIfEnabled rule ruleConfig =
constructRuleIfEnabledBase onlyEnabled rule ruleConfig

let constructRuleWithConfig (rule: 'TRuleConfig -> 'TRule) (ruleConfig: RuleConfig<'TRuleConfig>): Option<'TRule> =
constructRuleWithConfigBase onlyEnabled rule ruleConfig

let allRules =
[|
config.TypedItemSpacing |> Option.bind (constructRuleWithConfig TypedItemSpacing.rule)
Expand Down
4 changes: 2 additions & 2 deletions src/FSharpLint.Core/Application/Lint.fs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ module Lint =
| Some(x) -> not x.IsCancellationRequested
| None -> true

let enabledRules = Configuration.flattenConfig lintInfo.Configuration
let enabledRules = Configuration.flattenConfig lintInfo.Configuration true

let lines = String.toLines fileInfo.Text |> Array.map (fun (line, _, _) -> line)
let allRuleNames =
Expand Down Expand Up @@ -371,7 +371,7 @@ module Lint =
}

/// Gets a FSharpLint Configuration based on the provided ConfigurationParam.
let private getConfig (configParam:ConfigurationParam) =
let getConfig (configParam:ConfigurationParam) =
match configParam with
| Configuration config -> Ok config
| FromFile filePath ->
Expand Down
4 changes: 3 additions & 1 deletion src/FSharpLint.Core/Application/Lint.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,6 @@ module Lint =

/// Lints an F# file that has already been parsed using
/// `FSharp.Compiler.Services` in the calling application.
val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult
val lintParsedFile : optionalParams:OptionalLintParameters -> parsedFileInfo:ParsedFileInformation -> filePath:string -> LintResult

val getConfig : ConfigurationParam -> Result<Configuration,string>
3 changes: 3 additions & 0 deletions src/FSharpLint.Core/Text.resx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@
<data name="ConsoleStartingFile" xml:space="preserve">
<value>========== Linting {0} ==========</value>
</data>
<data name="ConsoleApplyingSuggestedFixFile" xml:space="preserve">
<value>========== Applying fixes ==========</value>
</data>
<data name="ConsoleMSBuildFailedToLoadProjectFile" xml:space="preserve">
<value>MSBuild could not load the project file {0} because: {1}</value>
</data>
Expand Down
Loading