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
5 changes: 4 additions & 1 deletion src/Fable.Cli/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) =
Expand Down
6 changes: 4 additions & 2 deletions src/Fable.Compiler/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -319,7 +320,8 @@ module CodeServices =
opts,
fableLibDir,
crackerResponse.OutputType,
?outDir = cliArgs.OutDir
?outDir = cliArgs.OutDir,
sourceReader = sourceReader
)

let outputPath = Path.ChangeExtension(currentFile, ".js")
Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Babel/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Beam/Fable2Beam.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Dart/Fable2Dart.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion src/Fable.Transforms/Dart/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
2 changes: 2 additions & 0 deletions src/Fable.Transforms/Fable.Transforms.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<Compile Include="Global/Metadata.fs" />
<Compile Include="Global/Prelude.fs" />
<Compile Include="Global/Compiler.fs" />
<Compile Include="Global/WarningSuppression.fs" />
<Compile Include="Global/WarningCodes.fs" />
<Compile Include="Global/Naming.fs" />
<Compile Include="Python/Prelude.fs" />
<Compile Include="MonadicTrampoline.fs" />
Expand Down
3 changes: 2 additions & 1 deletion src/Fable.Transforms/Global/Compiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/Fable.Transforms/Global/WarningCodes.fs
Original file line number Diff line number Diff line change
@@ -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

[<Literal>]
let private CultureInfoIgnored = "FABLE0001"

[<Literal>]
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"
231 changes: 231 additions & 0 deletions src/Fable.Transforms/Global/WarningSuppression.fs
Original file line number Diff line number Diff line change
@@ -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<string>
| SpecificDisabled of Set<string>

type private Directive =
| DisableLine of codes: Set<string> option * line: int
| DisableNextLine of codes: Set<string> option * line: int
| Disable of codes: Set<string> option * line: int
| Enable of codes: Set<string> 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<System.Text.StringBuilder>()
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<int, Set<string> 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<string> 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<Directive>()
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<string> 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
}
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Php/Fable2Php.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Python/PythonCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading
Loading