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
211 changes: 174 additions & 37 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 @@ -19,18 +21,28 @@ type internal FileType =
| File = 3
| Source = 4

type ExitCode =
| Error = -1
| Success = 0
| NoSuchRuleName = 1
| NoFix = 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 fixes for specified rule name or names (comma separated)."
| Version -> "Prints current version."

// TODO: investigate erroneous warning on this type definition
Expand All @@ -45,10 +57,33 @@ 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 fix 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
ShouldFix: bool
MaybeRuleName: string option
}

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

// can't extract inner functions because they modify exitCode variable
// fsharplint:disable MaxLinesInFunction
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 @@ -87,56 +124,156 @@ let private start (arguments:ParseResults<ToolArgs>) (toolsPath:Ionide.ProjInfo.
output.WriteInfo $"Current version: {version}"
Environment.Exit 0

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

match arguments.GetSubCommand() with
| Lint lintArgs ->
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 (target: string) (ruleName: string) = function
| LintResult.Success warnings ->
String.Format(Resources.GetString "ConsoleApplyingFixFile", target) |> output.WriteInfo
let increment = 1
let noFixIncrement = 0

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 countFixes (element: Suggestion.LintWarning) =
let sourceCode = File.ReadAllText element.FilePath
if String.Equals(ruleName, element.RuleName, StringComparison.InvariantCultureIgnoreCase) then
match element.Details.Fix with
| Some lazyFix ->
match lazyFix.Force() with
| Some fix ->
let updatedSourceCode =
let builder = StringBuilder(sourceCode.Length + fix.ToText.Length)
let firstPart =
sourceCode.AsSpan(
0,
(ExpressionUtilities.findPos fix.FromRange.Start sourceCode).Value
)
let secondPart =
sourceCode.AsSpan
(ExpressionUtilities.findPos fix.FromRange.End sourceCode).Value
builder
.Append(firstPart)
.Append(fix.ToText)
.Append(secondPart)
.ToString()
File.WriteAllText(
element.FilePath,
updatedSourceCode,
Encoding.UTF8)
| _ -> ()
increment
| None -> noFixIncrement
else
noFixIncrement

let lintConfig = lintArgs.TryGetResult Lint_Config
let countFix =
warnings |> List.sumBy countFixes
outputWarnings warnings

let configParam =
match lintConfig with
if countFix > 0 then
exitCode <- ExitCode.Success
else
exitCode <- ExitCode.NoFix

| LintResult.Failure failure -> handleError ExitCode.Error failure.Description

let linting (args: LintingArgs) =
try
let lintResult =
match args.FileType with
| FileType.File -> Lint.lintFile args.LintParams args.Target
| FileType.Source -> Lint.lintSource args.LintParams args.Target
| FileType.Solution -> Lint.lintSolution args.LintParams args.Target toolsPath
| FileType.Project
| _ -> Lint.lintProject args.LintParams args.Target toolsPath
if args.ShouldFix then
match args.MaybeRuleName with
| Some ruleName -> handleFixResult args.Target ruleName lintResult
| None -> exitCode <- ExitCode.NoSuchRuleName
else
handleLintResult lintResult
with
| exn ->
let target = if args.FileType = FileType.Source then "source" else args.Target
$"Lint failed while analysing %s{target}.{Environment.NewLine}Failed with: %s{exn.Message}{Environment.NewLine}Stack trace: {exn.StackTrace}"
|> 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 lintParams =
{ CancellationToken = None
ReceivedWarning = Some output.WriteWarning
Configuration = configParam
ReportLinterProgress = Some (parserProgress output) }
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)

try
let lintResult =
match fileType with
| FileType.File -> Lint.lintFile lintParams target
| FileType.Source -> Lint.lintSource lintParams target
| FileType.Solution -> Lint.lintSolution lintParams target toolsPath
| FileType.Project
| _ -> Lint.lintProject lintParams target toolsPath
handleLintResult 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}"
| _ -> ()
linting
{ FileType = fileType
LintParams = lintParams
Target = target
ToolsPath = toolsPath
ShouldFix = false
MaybeRuleName = None }

let applyFix (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 allRules =
match getConfig fixParams.Configuration with
| Ok config -> Some (Configuration.flattenConfig config false)
| _ -> None

exitCode
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 = fileType
LintParams = fixParams
Target = target
ToolsPath = toolsPath
ShouldFix = true
MaybeRuleName = Some ruleName }
else
sprintf "Rule '%s' does not exist." ruleName |> (handleError ExitCode.NoSuchRuleName)

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

int exitCode
// fsharplint:enable MaxLinesInFunction

/// Must be called only once per process.
/// We're calling it globally so we can call main multiple times from our tests.
let toolsPath = Ionide.ProjInfo.Init.init (DirectoryInfo <| Directory.GetCurrentDirectory()) None
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 @@ -105,13 +105,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 @@ -663,7 +670,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 @@ -673,6 +680,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 @@ -237,7 +237,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 @@ -423,7 +423,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 @@ -165,4 +165,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>
7 changes: 2 additions & 5 deletions src/FSharpLint.Core/Framework/Suggestion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ 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

type Fix = {
/// Location of the text to be replaced.
FromRange:Range

Expand All @@ -25,7 +22,7 @@ type WarningDetails = {
Message:string

/// Information to provide an automated fix.
SuggestedFix:Lazy<SuggestedFix option> option
Fix:Lazy<Fix option> option

/// Type checks to be performed to confirm this suggestion is valid.
/// Suggestion is only considered valid when all type checks resolve to true.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ let rec checkExpression (expression: SynExpr) (range: range) (continuation: unit
{
Range = range
Message = Resources.GetString "RulesAsyncExceptionWithoutReturn"
SuggestedFix = None
Fix = None
TypeChecks = List.Empty
}
| SynExpr.App (_, _, funcExpr, _, range) ->
Expand Down
Loading
Loading