diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 9adcfd787..c88307ab1 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -356,6 +356,8 @@ type FsWatcher(delayMs: int) = type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFiles: Fable.Compiler.File array) = + let sourceReader = lazy (snd (Fable.Compiler.File.MakeSourceReader sourceFiles)) + member _.CliArgs = cliArgs member _.ProjectFile = cliArgs.ProjectFile member _.FableOptions = cliArgs.CompilerOptions @@ -392,7 +394,8 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi fableLibDir, crackerResponse.OutputType, ?outDir = cliArgs.OutDir, - ?watchDependencies = watchDependencies + ?watchDependencies = watchDependencies, + sourceReader = sourceReader.Value ) member _.MapSourceFiles(f) = diff --git a/src/Fable.Compiler/Library.fs b/src/Fable.Compiler/Library.fs index e7b6c7c6d..427789aab 100644 --- a/src/Fable.Compiler/Library.fs +++ b/src/Fable.Compiler/Library.fs @@ -180,7 +180,8 @@ module CodeServices = opts, fableLibDir, crackerResponse.OutputType, - ?outDir = cliArgs.OutDir + ?outDir = cliArgs.OutDir, + sourceReader = sourceReader ) // TODO: make it configurable if FableTransforms.transformFile is applied? @@ -319,7 +320,8 @@ module CodeServices = opts, fableLibDir, crackerResponse.OutputType, - ?outDir = cliArgs.OutDir + ?outDir = cliArgs.OutDir, + sourceReader = sourceReader ) let outputPath = Path.ChangeExtension(currentFile, ".js") diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index e9312c080..3fe120a95 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -4987,8 +4987,8 @@ module Compiler = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = BabelCompiler(com) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 2f49d7df7..a5ac265d2 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -3760,8 +3760,8 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = member _.GetInlineExpr(key) = com.GetInlineExpr(key) member _.AddWatchDependency(file) = com.AddWatchDependency(file) - member _.AddLog(msg, severity, ?range, ?fileName, ?tag) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName, ?tag, ?code) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) } let forms = diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index 788a68ad0..26114be2d 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -3117,8 +3117,8 @@ module Compiler = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = DartCompiler(com) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index aed42ea52..6fcf3bdbd 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1395,7 +1395,8 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetEnumerator", Some c, _ -> stringToCharSeq c |> getEnumerator com r t |> Some | ("Contains" | "StartsWith" | "EndsWith" as meth), Some c, arg :: _ -> if List.isMultiple args then - addWarning com ctx.InlinePath r $"String.%s{meth}: second argument is ignored" + WarningCodes.stringSecondArgumentIgnored meth + |> addWarningWithCode com ctx.InlinePath r Helper.InstanceCall(c, Naming.lowerFirst meth, t, [ arg ], ?loc = r) |> Some | ReplaceName [ "ToUpper", "toUpperCase" diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 4e25f6b74..49bd3c360 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2857,8 +2857,8 @@ type FableCompiler(com: Compiler) = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let rec attachClassMembers (com: FableCompiler) = diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 738cab2bb..d2a0d0331 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -5,6 +5,8 @@ + + diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 7b2b2ac97..cb33b5e9a 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -86,7 +86,8 @@ type Compiler = abstract AddWatchDependency: file: string -> unit abstract AddLog: - msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string -> unit + msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string * ?code: string -> + unit type InlineExprLazy(f: Compiler -> InlineExpr) = let mutable value: InlineExpr voption = ValueNone diff --git a/src/Fable.Transforms/Global/WarningCodes.fs b/src/Fable.Transforms/Global/WarningCodes.fs new file mode 100644 index 000000000..002e5b9be --- /dev/null +++ b/src/Fable.Transforms/Global/WarningCodes.fs @@ -0,0 +1,23 @@ +/// Central registry of stable codes + messages for `addWarningWithCode`: one function per +/// warning, so call sites sharing the same warning (e.g. StartsWith/EndsWith, JS/Python) can't +/// drift into different codes or wording. Codes are never reused/renumbered once published. +/// Usage: `WarningCodes.someWarning arg1 arg2 |> addWarningWithCode com inlinePath range`. +module Fable.Transforms.WarningCodes + +[] +let private CultureInfoIgnored = "FABLE0001" + +[] +let private StringSecondArgumentIgnored = "FABLE0002" + +/// `String.StartsWith`/`EndsWith` with a `CultureInfo` argument: the comparison always runs +/// with the target's default culture rules, the argument is accepted but has no effect. +/// Used in both the JS/TS and Python replacements. +let cultureInfoIgnored () = + CultureInfoIgnored, "CultureInfo argument is ignored" + +/// `String.Contains`/`StartsWith`/`EndsWith` called with a `StringComparison` argument on the +/// Dart target: only the comparison itself is honored, `methodName` fills in which one so the +/// message stays specific (e.g. "String.Contains: second argument is ignored"). +let stringSecondArgumentIgnored (methodName: string) = + StringSecondArgumentIgnored, $"String.{methodName}: second argument is ignored" diff --git a/src/Fable.Transforms/Global/WarningSuppression.fs b/src/Fable.Transforms/Global/WarningSuppression.fs new file mode 100644 index 000000000..8a4307e48 --- /dev/null +++ b/src/Fable.Transforms/Global/WarningSuppression.fs @@ -0,0 +1,231 @@ +/// Computes which diagnostics `// fable-disable/-enable...` comments suppress (ESLint's +/// disable-line/next-line/block model), via real comment tokens - not raw text matching. +module Fable.Transforms.WarningSuppression + +open System.Text.RegularExpressions +open FSharp.Compiler.Tokenization + +type private BlockState = + | NoneDisabled + | AllDisabledExcept of Set + | SpecificDisabled of Set + +type private Directive = + | DisableLine of codes: Set option * line: int + | DisableNextLine of codes: Set option * line: int + | Disable of codes: Set option * line: int + | Enable of codes: Set option * line: int + +let private directiveRegex = + Regex(@"^fable-(disable-next-line|disable-line|disable|enable)(?:\s+(.+))?$", RegexOptions.Compiled) + +let private parseCodes (s: string) = + let codes = + s.Split([| ' '; ','; '\t' |], System.StringSplitOptions.RemoveEmptyEntries) + |> Set.ofArray + + if Set.isEmpty codes then + None + else + Some codes + +let private stripCommentMarkers (raw: string) = + let raw = + if raw.StartsWith("//", System.StringComparison.Ordinal) then + raw.Substring(2) + elif raw.StartsWith("(*", System.StringComparison.Ordinal) then + raw.Substring(2) + else + raw + + let raw = + if raw.EndsWith("*)", System.StringComparison.Ordinal) then + raw.Substring(0, raw.Length - 2) + else + raw + + raw.Trim() + +let private tryParseDirective (line: int) (commentText: string) : Directive option = + let text = stripCommentMarkers commentText + let m = directiveRegex.Match(text) + + if not m.Success then + None + else + let codes = + if m.Groups[2].Success then + parseCodes m.Groups[2].Value + else + None + + match m.Groups[1].Value with + | "disable-line" -> Some(DisableLine(codes, line)) + | "disable-next-line" -> Some(DisableNextLine(codes, line)) + | "disable" -> Some(Disable(codes, line)) + | "enable" -> Some(Enable(codes, line)) + | _ -> None + +/// Gathers each line's comment token runs as plain text (a line can have more than one, e.g. a +/// block comment then a trailing line comment); also returns the lexer state to carry into the +/// next line, needed to resume correctly inside multi-line block comments/strings. +let private scanLineComments + (tokenizer: FSharpLineTokenizer) + (initialState: FSharpTokenizerLexState) + (line: string) + : string list * FSharpTokenizerLexState + = + // Each finished run is one comment on the line; `current` is the run being built. + let runs = ResizeArray() + let mutable current: System.Text.StringBuilder option = None + + // Pull tokens one at a time, threading the lexer state (needed across lines too). + let rec loop state = + match tokenizer.ScanToken(state) with + | Some(tok: FSharpTokenInfo), state2 -> + if tok.ColorClass = FSharpTokenColorKind.Comment then + let text = line.Substring(tok.LeftColumn, tok.RightColumn - tok.LeftColumn + 1) + + match current with + // Still inside the same comment: glue this token onto the current run. + | Some sb -> sb.Append(text) |> ignore + // First comment token after non-comment text: start a new run. + | None -> + let sb = System.Text.StringBuilder(text: string) + current <- Some sb + runs.Add(sb) + else + // Non-comment token: close the current run, if any (e.g. code between two comments). + current <- None + + loop state2 + // No more tokens on this line: return the final state for the next line. + | None, state2 -> state2 + + let endState = loop initialState + // Materialize each run's text; endState lets the caller resume correctly on the next line. + (runs |> Seq.map string |> List.ofSeq), endState + +/// Computed, queryable suppression info for a single source file. +type FileSuppressions = + private + { + /// 1-based line -> codes suppressed specifically on that line (None = all codes) + LineOnly: Map option> + /// index (line - 1) -> block-disable state as of (and including) that line + BlockAtLine: BlockState[] + } + + /// Is a diagnostic with the given code (None = no code assigned to it) suppressed on this line? + member this.IsSuppressed(line: int, code: string option) = + let lineSuppressed = + match Map.tryFind line this.LineOnly with + | Some None -> true + | Some(Some codes) -> code |> Option.map codes.Contains |> Option.defaultValue false + | None -> false + + let blockSuppressed = + if line < 1 || line > this.BlockAtLine.Length then + false + else + match this.BlockAtLine[line - 1] with + | NoneDisabled -> false + | AllDisabledExcept enabled -> + match code with + | None -> true + | Some c -> not (Set.contains c enabled) + | SpecificDisabled disabled -> + match code with + | None -> false + | Some c -> Set.contains c disabled + + lineSuppressed || blockSuppressed + + static member Empty = + { + LineOnly = Map.empty + BlockAtLine = [||] + } + +let private transition (state: BlockState) (codes: Set option) (isDisable: bool) = + match isDisable, codes, state with + | true, None, _ -> AllDisabledExcept Set.empty + | true, Some codes, NoneDisabled -> SpecificDisabled codes + | true, Some codes, SpecificDisabled s -> SpecificDisabled(Set.union s codes) + | true, Some codes, AllDisabledExcept ex -> AllDisabledExcept(Set.difference ex codes) + | false, None, _ -> NoneDisabled + | false, Some _, NoneDisabled -> NoneDisabled + | false, Some codes, SpecificDisabled s -> SpecificDisabled(Set.difference s codes) + | false, Some codes, AllDisabledExcept ex -> AllDisabledExcept(Set.union ex codes) + +/// Scans the given source text for `fable-disable*`/`fable-enable*` comments and builds a +/// queryable `FileSuppressions` snapshot. Meant to be computed once per file and cached. +let compute (source: string) : FileSuppressions = + let lines = source.Replace("\r\n", "\n").Split('\n') + + if lines.Length = 0 then + FileSuppressions.Empty + else + let sourceTok = FSharpSourceTokenizer([], None, None, None) + let directives = ResizeArray() + let mutable state = FSharpTokenizerLexState.Initial + + for i in 0 .. lines.Length - 1 do + let lineNo = i + 1 + let tokenizer = sourceTok.CreateLineTokenizer(lines[i]) + let runs, newState = scanLineComments tokenizer state lines[i] + state <- newState + + for run in runs do + tryParseDirective lineNo run |> Option.iter directives.Add + + let lineOnly = + (Map.empty, directives) + ||> Seq.fold (fun acc d -> + let merge line (codes: Set option) = + let merged = + match Map.tryFind line acc, codes with + | Some None, _ + | _, None -> None + | None, Some c -> Some c + | Some(Some existing), Some c -> Some(Set.union existing c) + + Map.add line merged acc + + match d with + | DisableLine(codes, line) -> merge line codes + | DisableNextLine(codes, line) -> merge (line + 1) codes + | Disable _ + | Enable _ -> acc + ) + + let blockDirectivesByLine = + directives + |> Seq.choose ( + function + | Disable(codes, line) -> Some(line, codes, true) + | Enable(codes, line) -> Some(line, codes, false) + | DisableLine _ + | DisableNextLine _ -> None + ) + |> Seq.groupBy (fun (line, _, _) -> line) + |> Map.ofSeq + + // Filling this with hundreds/thousands of NoneDisabled is nearly free: F# compiles a + // nullary DU case to a singleton, so every slot is a pointer to the same object. + let blockAtLine = Array.create lines.Length NoneDisabled + let mutable current = NoneDisabled + + for lineNo in 1 .. lines.Length do + match Map.tryFind lineNo blockDirectivesByLine with + | Some ds -> + for _, codes, isDisable in ds do + current <- transition current codes isDisable + | None -> () + + blockAtLine[lineNo - 1] <- current + + { + LineOnly = lineOnly + BlockAtLine = blockAtLine + } diff --git a/src/Fable.Transforms/Php/Fable2Php.fs b/src/Fable.Transforms/Php/Fable2Php.fs index 4afbb5ee5..2aa0c1970 100644 --- a/src/Fable.Transforms/Php/Fable2Php.fs +++ b/src/Fable.Transforms/Php/Fable2Php.fs @@ -2138,8 +2138,8 @@ type PhpCompiler(com: Fable.Compiler) = member this.WillPrecompileInlineFunction(file) = com.WillPrecompileInlineFunction(file) - member this.AddLog(msg, severity, rang, fileName, tag) = - com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag) + member this.AddLog(msg, severity, rang, fileName, tag, code) = + com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag, ?code = code) member this.AddWatchDependency(file) = com.AddWatchDependency(file) diff --git a/src/Fable.Transforms/Python/PythonCompiler.fs b/src/Fable.Transforms/Python/PythonCompiler.fs index 63563c02b..7430a9a1e 100644 --- a/src/Fable.Transforms/Python/PythonCompiler.fs +++ b/src/Fable.Transforms/Python/PythonCompiler.fs @@ -137,8 +137,8 @@ type PythonCompiler(com: Compiler) = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = PythonCompiler(com) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 5676b8c46..8e3d24738 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1512,7 +1512,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "string", "starts_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored () |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "string", "starts_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1524,7 +1524,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "string", "ends_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored () |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "string", "ends_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index aef89aa1e..7b12349c5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1723,7 +1723,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored () |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1733,7 +1733,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored () |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 58aeba693..f5f846c6d 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -5735,8 +5735,8 @@ module Compiler = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = RustCompiler(com) diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 65672dec4..448d9ca3e 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -257,15 +257,17 @@ type LogEntry = Severity: Severity Range: SourceLocation option FileName: string option + Code: string option } - static member Make(severity, msg, ?fileName, ?range, ?tag) = + static member Make(severity, msg, ?fileName, ?range, ?tag, ?code) = { Message = msg Tag = defaultArg tag "FABLE" Severity = severity Range = range FileName = fileName + Code = code } static member MakeError(msg, ?fileName, ?range, ?tag) = @@ -283,7 +285,8 @@ type CompilerImpl ?outDir: string, ?watchDependencies: HashSet, ?logs: ResizeArray, - ?isPrecompilingInlineFunction: bool + ?isPrecompilingInlineFunction: bool, + ?sourceReader: SourceReader ) = @@ -291,6 +294,23 @@ type CompilerImpl let outType = defaultArg outType OutputType.Exe let logs = Option.defaultWith ResizeArray logs let fableLibraryDir = fableLibDir.TrimEnd('/') + let suppressionsCache = Dictionary() + + let getSuppressions fileName = + match suppressionsCache.TryGetValue(fileName) with + | true, s -> s + | false, _ -> + let suppressions = + match sourceReader with + | None -> WarningSuppression.FileSuppressions.Empty + | Some read -> + try + (snd (read fileName)).Value |> WarningSuppression.compute + with _ -> + WarningSuppression.FileSuppressions.Empty + + suppressionsCache[fileName] <- suppressions + suppressions member _.Logs = logs.ToArray() @@ -333,7 +353,8 @@ type CompilerImpl ?outDir = outDir, ?watchDependencies = watchDependencies, logs = logs, - isPrecompilingInlineFunction = true + isPrecompilingInlineFunction = true, + ?sourceReader = sourceReader ) member _.GetImplementationFile(fileName) = @@ -387,6 +408,25 @@ type CompilerImpl | Some watchDependencies when file <> currentFile -> watchDependencies.Add(file) |> ignore | _ -> () - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag) - |> logs.Add + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + // Only warnings can be suppressed, errors always surface (matches F#'s own #nowarn) + let isSuppressed = + severity = Severity.Warning + && ( + match range with + | None -> false + | Some(r: SourceLocation) -> + let file = + match fileName with + | Some f -> f + | None -> + match r.File with + | Some f -> f + | None -> currentFile + + (getSuppressions file).IsSuppressed(r.start.line, code) + ) + + if not isSuppressed then + LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) + |> logs.Add diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index c5cc988a7..9d796cded 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -668,7 +668,14 @@ module Log = FromRange: SourceLocation option } - let private addLog (com: Compiler) (inlinePath: InlinePath list) (range: SourceLocation option) msg severity = + let private addLogWithCode + (com: Compiler) + (inlinePath: InlinePath list) + (range: SourceLocation option) + msg + severity + (code: string option) + = let printInlineSource fromPath (p: InlinePath) = let path = Path.getRelativeFileOrDirPath false fromPath false p.FromFile @@ -685,11 +692,20 @@ module Log = file, msg + " - Inline call from " + inlinePath | [] -> range |> Option.bind (fun r -> r.File) |> Option.defaultValue com.CurrentFile, msg - com.AddLog(msg, severity, ?range = range, fileName = actualFile) + com.AddLog(msg, severity, ?range = range, fileName = actualFile, ?code = code) + + let private addLog (com: Compiler) (inlinePath: InlinePath list) (range: SourceLocation option) msg severity = + addLogWithCode com inlinePath range msg severity None let addWarning (com: Compiler) inlinePath range warning = addLog com inlinePath range warning Severity.Warning + /// Same as `addWarning`, but tags the warning with a stable code (e.g. "FABLE0001") so it + /// can be suppressed via `// fable-disable-line/-next-line/-enable CODE` comments. + /// Meant to be used as `WarningCodes.someWarning arg1 arg2 |> addWarningWithCode com inlinePath range`. + let addWarningWithCode (com: Compiler) inlinePath range ((code, warning): string * string) = + addLogWithCode com inlinePath range warning Severity.Warning (Some code) + let addError (com: Compiler) inlinePath range error = addLog com inlinePath range error Severity.Error diff --git a/src/fable-standalone/src/Fable.Standalone.fsproj b/src/fable-standalone/src/Fable.Standalone.fsproj index 64ac4fa02..f6bc334cd 100644 --- a/src/fable-standalone/src/Fable.Standalone.fsproj +++ b/src/fable-standalone/src/Fable.Standalone.fsproj @@ -22,6 +22,8 @@ + + diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 24420f1bd..402bd7f67 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -125,4 +125,95 @@ type MyClass() = compile source |> Assert.Is.success |> ignore + + testCase "CultureInfo argument warning is not suppressed by default" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "The same code covers both StartsWith and EndsWith call sites" <| fun _ -> + // Regression test for a real bug: StartsWith and EndsWith both raise the same logical + // "CultureInfo argument is ignored" warning from two separate call sites in + // Replacements.fs, sharing WarningCodes.CultureInfoIgnored. Both must be suppressible + // by one code, and (in Python's Replacements.fs) this used to have no code at all. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0001 +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0001 +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable-line suppresses a warning on the same line" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0001 +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable-next-line suppresses a warning on the following line" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0001 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable/fable-enable suppresses warnings in a block" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable FABLE0001 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore +// fable-enable FABLE0001 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Are.warnings 1 + |> ignore + + testCase "A mismatched code does not suppress the warning" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line SOME_OTHER_CODE +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "A bare fable-disable-line suppresses regardless of code" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "A string literal that looks like a directive is not treated as one" <| fun _ -> + let source = + """ +open System.Globalization +let s = "// fable-disable-line FABLE0001" +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore ]