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
]