Skip to content
Open
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
222 changes: 176 additions & 46 deletions src/FSharpLint.Console/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ open Argu
open System
open System.IO
open System.Reflection
open System.Linq
open System.Text
open FSharpLint.Framework
open FSharpLint.Application

Expand All @@ -23,19 +25,25 @@ type internal FileType =
type ExitCode =
| Failure = -1
| Success = 0
| NoSuchRuleName = 1
| NoSuggestedFix = 2

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>
| Version
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)."
| Version -> "Prints current version."

// TODO: investigate erroneous warning on this type definition
Expand All @@ -50,10 +58,32 @@ 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."

// 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
// 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
// fsharplint:enable UnionCasesNames

type private LintingArgs =
{
FileType: FileType
LintParams: OptionalLintParameters
Target: string
ToolsPath: Ionide.ProjInfo.Types.ToolsPath
RuleNameToApplySuggestedFixFrom: string option
}

/// Expands a wildcard pattern to a list of matching files.
/// Supports recursive search using ** (e.g., "**/*.fs" or "src/**/*.fs")
let internal expandWildcard (pattern:string) =
Expand Down Expand Up @@ -117,69 +147,167 @@ let internal inferFileType (target:string) =
else
FileType.Source

let private lint
(lintArgs: ParseResults<LintArgs>)
(output: Output.IOutput)
(toolsPath:Ionide.ProjInfo.Types.ToolsPath)
: ExitCode =
let mutable exitCode = ExitCode.Success
let private outputWarnings (output: Output.IOutput) (warnings: List<Suggestion.LintWarning>) =
String.Format(Resources.GetString "ConsoleFinished", List.length warnings)
|> output.WriteInfo

let private handleLintResult (output: Output.IOutput) (lintResult: LintResult) =
match lintResult with
| LintResult.Success warnings ->
outputWarnings output warnings
if List.isEmpty warnings |> not then
ExitCode.Failure
else
ExitCode.Success
| LintResult.Failure failure ->
output.WriteError failure.Description
ExitCode.Failure

let private getParams (output: Output.IOutput) config =
let paramConfig =
match config with
| Some configPath -> FromFile configPath
| None -> Default

let handleError (str:string) =
output.WriteError str
exitCode <- ExitCode.Failure
{ CancellationToken = None
ReceivedWarning = Some output.WriteWarning
Configuration = paramConfig
ReportLinterProgress = parserProgress output |> Some }

let handleLintResult = function
| LintResult.Success(warnings) ->
String.Format(Resources.GetString("ConsoleFinished"), List.length warnings)
|> output.WriteInfo
if not (List.isEmpty warnings) then
exitCode <- ExitCode.Failure
| LintResult.Failure(failure) ->
handleError failure.Description
let private handleFixResult (output: Output.IOutput) (target: string) (ruleName: string) (lintResult: LintResult) : ExitCode =
match lintResult with
| LintResult.Success warnings ->
String.Format(Resources.GetString "ConsoleApplyingSuggestedFixFile", target) |> output.WriteInfo
let increment = 1
let noFixIncrement = 0

let lintConfig = lintArgs.TryGetResult Lint_Config
let countFixes (element: Suggestion.LintWarning) =
let sourceCode = File.ReadAllText element.FilePath
if String.Equals(ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
match element.Details.SuggestedFix with
| Some lazySuggestedFix ->
match lazySuggestedFix.Force() with
| Some suggestedFix ->
let updatedSourceCode =
let builder = StringBuilder(sourceCode.Length + suggestedFix.ToText.Length)
let firstPart =
sourceCode.AsSpan(
0,
(ExpressionUtilities.findPos suggestedFix.FromRange.Start sourceCode)
|> Option.defaultWith
(fun () -> failwith "Could not find index from position (suggestedFix.FromRange.Start)")
)
let secondPart =
sourceCode.AsSpan
(ExpressionUtilities.findPos suggestedFix.FromRange.End sourceCode
|> Option.defaultWith
(fun () -> failwith "Could not find index from position (suggestedFix.FromRange.End)"))
builder
.Append(firstPart)
.Append(suggestedFix.ToText)
.Append(secondPart)
.ToString()
File.WriteAllText(
element.FilePath,
updatedSourceCode,
Encoding.UTF8)
| _ -> ()
increment
| None -> noFixIncrement
else
noFixIncrement

let configParam =
match lintConfig with
| Some configPath -> FromFile configPath
| None -> Default
let countSuggestedFix =
warnings |> List.sumBy countFixes
outputWarnings output warnings

let lintParams =
{
CancellationToken = None
ReceivedWarning = Some output.WriteWarning
Configuration = configParam
ReportLinterProgress = Some (parserProgress output)
}
if countSuggestedFix > 0 then
ExitCode.Success
else
ExitCode.NoSuggestedFix

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

let private performLinting (output: Output.IOutput) (args: LintingArgs) =
try
let lintResult =
match fileType with
| FileType.File -> Lint.asyncLintFile lintParams target |> Async.RunSynchronously
| FileType.Source -> Lint.asyncLintSource lintParams target |> Async.RunSynchronously
| FileType.Solution -> Lint.asyncLintSolution lintParams target toolsPath |> Async.RunSynchronously
match args.FileType with
| FileType.File -> Lint.asyncLintFile args.LintParams args.Target |> Async.RunSynchronously
| FileType.Source -> Lint.asyncLintSource args.LintParams args.Target |> Async.RunSynchronously
| FileType.Solution -> Lint.asyncLintSolution args.LintParams args.Target args.ToolsPath |> Async.RunSynchronously
| FileType.Wildcard ->
output.WriteInfo "Wildcard detected, but not recommended. Using a project (slnx/sln/fsproj) can detect more issues."
let files = expandWildcard target
let files = expandWildcard args.Target
if List.isEmpty files then
output.WriteInfo $"No files matching pattern '%s{target}' were found."
output.WriteInfo $"No files matching pattern '%s{args.Target}' were found."
LintResult.Success List.empty
else
output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{target}'."
Lint.asyncLintFiles lintParams files |> Async.RunSynchronously
output.WriteInfo $"Found %d{List.length files} file(s) matching pattern '%s{args.Target}'."
Lint.asyncLintFiles args.LintParams files |> Async.RunSynchronously
| FileType.Project
| _ -> Lint.asyncLintProject lintParams target toolsPath |> Async.RunSynchronously
handleLintResult lintResult
| _ -> Lint.asyncLintProject args.LintParams args.Target args.ToolsPath |> Async.RunSynchronously

match args.RuleNameToApplySuggestedFixFrom with
| Some ruleName -> handleFixResult output args.Target ruleName lintResult
| None -> handleLintResult output lintResult
with
| exn ->
let target = if fileType = FileType.Source then "source" else target
handleError
$"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
let target = if args.FileType = FileType.Source then "source" else args.Target
output.WriteError $"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
ExitCode.Failure

exitCode
let private lint
(lintArgs: ParseResults<LintArgs>)
(output: Output.IOutput)
(toolsPath:Ionide.ProjInfo.Types.ToolsPath)
: ExitCode =
let lintConfig = lintArgs.TryGetResult Lint_Config

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

performLinting
output
{ FileType = fileType
LintParams = lintParams
Target = target
ToolsPath = toolsPath
RuleNameToApplySuggestedFixFrom = None }

let private applySuggestedFix (fixArgs: ParseResults<FixArgs>) (output: Output.IOutput) toolsPath =
let fixParams = getParams output None
let ruleName, target = fixArgs.GetResult Fix_Target
let fileType = fixArgs.TryGetResult Fix_File_Type |> Option.defaultValue (inferFileType target)

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
performLinting
output
{ FileType = fileType
LintParams = fixParams
Target = target
ToolsPath = toolsPath
RuleNameToApplySuggestedFixFrom = Some ruleName }
else
output.WriteError <| sprintf "Rule '%s' does not exist." ruleName
ExitCode.NoSuchRuleName

let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.Types.ToolsPath) =
let output =
Expand All @@ -203,6 +331,8 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
match arguments.GetSubCommand() with
| Lint lintArgs ->
lint lintArgs output toolsPath
| Fix fixArgs ->
applySuggestedFix fixArgs output toolsPath
| _ ->
ExitCode.Failure

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 @@ -150,13 +150,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
Option.map rule ruleConfig.Config
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

let constructTypePrefixingRuleWithConfig rule (ruleConfig: RuleConfig<TypePrefixing.Config>) =
if ruleConfig.Enabled then
Expand Down Expand Up @@ -755,7 +762,7 @@ let findDeprecation config deprecatedAllRules allRules =
}

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

let constructRuleIfEnabled rule ruleConfig =
constructRuleIfEnabledBase onlyEnabled rule ruleConfig

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

let allRules =
Array.choose
id
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 @@ -239,7 +239,7 @@ module Lint =
| Some(value) -> not value.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 @@ -422,7 +422,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
2 changes: 2 additions & 0 deletions src/FSharpLint.Core/Application/Lint.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,5 @@ 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 getConfig : ConfigurationParam -> Result<Configuration,string>
3 changes: 0 additions & 3 deletions src/FSharpLint.Core/Framework/Suggestion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ open FSharp.Compiler.Text
/// Information for consuming applications to provide an automated fix for a lint suggestion.
[<NoEquality; NoComparison>]
type SuggestedFix = {
/// Text to be replaced.
FromText:string

/// Location of the text to be replaced.
FromRange:Range

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ let runner (args: AstNodeRuleParams) =
match (maybeFuncText, maybeArgText) with
| Some(funcText), Some(argText) ->
let replacementText = sprintf "%s %s" funcText argText
Some { FromText=args.FileContent; FromRange=range; ToText=replacementText }
Some { FromRange=range; ToText=replacementText }
| _ -> None)
errors ident.idRange (Some suggestedFix)
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let private checkForNamedPatternEqualsConstant (args:AstNodeRuleParams) pattern

ExpressionUtilities.tryFindTextOfRange constRange args.FileContent
|> Option.bind (fun constText ->
Some (lazy (Some { FromText = text; FromRange = fromRange; ToText = $"{constText} as {ident.idText}"}))
Some (lazy (Some { FromRange = fromRange; ToText = $"{constText} as {ident.idText}"}))
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ let private checkForBindingToAWildcard pattern range fileContent (expr: SynExpr)
{ Range = range
Message = Resources.GetString("RulesFavourIgnoreOverLetWildError")
SuggestedFix = Some (lazy (Some({ FromRange = letBindingRange
FromText = fileContent
ToText = sprintf "(%s) |> ignore" exprText })))
TypeChecks = List.Empty }
else
Expand Down
Loading
Loading