A project's packages, and diagnostics at their column
A file in a project is checked against the packages bjo fetched for it: the server installs the project's .bjo/roots, .bjo/frameworks and .bjo/nuget/root before its first check, through the compiler's ProjectInputs, so imports from packages check and definition, hover and completion reach them. A server serves one project, the first file's; a warning on a file's first line says when it cannot serve one as bjo would: packages not fetched, the roots changed since it started, or another project. The server no longer has the old Microsoft.Extensions.DependencyInjection .Abstractions next to it. The compiler excludes it, the exclusion does not pass through a project reference, and with it there every ASP.NET type failed to load, so bjoweb did not check. A diagnostic marks the form at the line and column the compiler names now: an atom or a string whole, a bracketed form to its closing bracket, and one over several lines as far as its head. The build no longer asks git for Source Link, which fails on this repository's SHA-256 objects. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-5-5) Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
parent
77b8a92e03
commit
cc62a8a360
6 changed files with 362 additions and 15 deletions
132
Check.fs
132
Check.fs
|
|
@ -18,8 +18,8 @@ type Found =
|
|||
IsError: bool
|
||||
Message: string }
|
||||
|
||||
/// `at file.bjo:12` or `at file.bjo:12:3`, as `Lexer.formatPos` and
|
||||
/// `Lexer.formatAt` write a location into a message.
|
||||
/// `at file.bjo:12:3`, as `Lexer.formatPos` and `Lexer.formatAt` write a
|
||||
/// location into a message, the column counted from 1, or `at file.bjo:12`.
|
||||
let private location = Regex(@"\n?\s*\bat (\S+?):(\d+)(?::(\d+))?\s*$", RegexOptions.Multiline)
|
||||
|
||||
let private firstLocation = Regex(@"\bat (\S+?):(\d+)(?::(\d+))?")
|
||||
|
|
@ -31,10 +31,104 @@ let private withoutLocations (message: string) = location.Replace(message, "").T
|
|||
let private wholeLine (line: int) =
|
||||
{| Line = line; Column = 0; EndLine = line + 1; EndColumn = 0 |}
|
||||
|
||||
let private isBreak (c: char) =
|
||||
Char.IsWhiteSpace c || "()[]{}\"';".Contains c
|
||||
|
||||
/// What to mark of the form that starts at `line` and `column`, both from 0:
|
||||
/// a string or an atom whole, a bracketed form to its closing bracket. A form
|
||||
/// over several lines is marked as far as its head, `(defun` or `(match`, so
|
||||
/// that an error in a definition does not mark all of it. `None` where the
|
||||
/// text has no form there.
|
||||
let private formAt (text: string) (line: int) (column: int) =
|
||||
let lines = text.Replace("\r\n", "\n").Split('\n')
|
||||
|
||||
if line >= lines.Length || column >= lines[line].Length then
|
||||
None
|
||||
else
|
||||
let here = lines[line]
|
||||
|
||||
// The end of the atom that starts at `i` on line `l`.
|
||||
let atomEnd (l: string) (i: int) =
|
||||
let mutable j = i
|
||||
|
||||
while j < l.Length && not (isBreak l[j]) do
|
||||
j <- j + 1
|
||||
|
||||
j
|
||||
|
||||
let single (endColumn: int) =
|
||||
Some {| Line = line; Column = column; EndLine = line; EndColumn = max (column + 1) endColumn |}
|
||||
|
||||
match here[column] with
|
||||
| '"' ->
|
||||
// To the closing quote, past escapes; to the end of the line when
|
||||
// the string goes on.
|
||||
let mutable j = column + 1
|
||||
|
||||
while j < here.Length && here[j] <> '"' do
|
||||
if here[j] = '\\' then j <- j + 1
|
||||
j <- j + 1
|
||||
|
||||
single (min here.Length (j + 1))
|
||||
| '('
|
||||
| '['
|
||||
| '{' ->
|
||||
// The closing bracket, counting brackets outside strings, comments
|
||||
// and character literals.
|
||||
let mutable depth = 0
|
||||
let mutable l = line
|
||||
let mutable i = column
|
||||
let mutable closedAt = None
|
||||
|
||||
while closedAt.IsNone && l < lines.Length do
|
||||
let s = lines[l]
|
||||
let mutable inString = false
|
||||
|
||||
while closedAt.IsNone && i < s.Length do
|
||||
let c = s[i]
|
||||
|
||||
if inString then
|
||||
if c = '\\' then i <- i + 1
|
||||
elif c = '"' then inString <- false
|
||||
elif c = '"' then
|
||||
inString <- true
|
||||
elif c = ';' then
|
||||
i <- s.Length
|
||||
elif c = '#' && i + 1 < s.Length && s[i + 1] = '\\' then
|
||||
i <- i + 2
|
||||
elif "([{".Contains c then
|
||||
depth <- depth + 1
|
||||
elif ")]}".Contains c then
|
||||
depth <- depth - 1
|
||||
if depth = 0 then closedAt <- Some(l, i + 1)
|
||||
|
||||
i <- i + 1
|
||||
|
||||
if closedAt.IsNone then
|
||||
l <- l + 1
|
||||
i <- 0
|
||||
|
||||
match closedAt with
|
||||
| Some(endLine, endColumn) when endLine = line -> single endColumn
|
||||
| _ ->
|
||||
// Over several lines, or never closed: the bracket and the
|
||||
// head after it.
|
||||
let mutable j = column + 1
|
||||
|
||||
while j < here.Length && Char.IsWhiteSpace here[j] do
|
||||
j <- j + 1
|
||||
|
||||
single (if j < here.Length && not (isBreak here[j]) then atomEnd here j else column + 1)
|
||||
| c when Char.IsWhiteSpace c -> None
|
||||
| _ -> single (atomEnd here column)
|
||||
|
||||
/// Where a diagnostic is, if it is in `file`. Anywhere else, such as in an
|
||||
/// imported module, it is `None`, and the caller puts it at the top of `file`
|
||||
/// with the location left in the message.
|
||||
let private placeIn (file: string) (d: Diagnostics.Diagnostic) =
|
||||
let private placeIn (text: string option) (file: string) (d: Diagnostics.Diagnostic) =
|
||||
let markFrom (line: int) (column: int) =
|
||||
text |> Option.bind (fun t -> formAt t line column)
|
||||
|
||||
let sameFile (other: string) =
|
||||
// A range carries the full path; a location read from a message only
|
||||
// the file name, which is all `Lexer.formatPos` writes.
|
||||
|
|
@ -51,10 +145,17 @@ let private placeIn (file: string) (d: Diagnostics.Diagnostic) =
|
|||
let endLine = max line (r.End.Line - 1)
|
||||
let endColumn = r.End.Column
|
||||
|
||||
// A range with no width is still something to underline: the rest of
|
||||
// its line.
|
||||
// A range with no width is still something to underline: the form
|
||||
// there, else the rest of its line. A range over several lines, which
|
||||
// is a whole declaration's, is marked as far as the form's head.
|
||||
if endLine = line && endColumn <= column then
|
||||
Some {| Line = line; Column = column; EndLine = line + 1; EndColumn = 0 |}
|
||||
match markFrom line column with
|
||||
| Some mark -> Some mark
|
||||
| None -> Some {| Line = line; Column = column; EndLine = line + 1; EndColumn = 0 |}
|
||||
elif endLine > line then
|
||||
match markFrom line column with
|
||||
| Some mark -> Some mark
|
||||
| None -> Some {| Line = line; Column = column; EndLine = endLine; EndColumn = endColumn |}
|
||||
else
|
||||
Some {| Line = line; Column = column; EndLine = endLine; EndColumn = endColumn |}
|
||||
| Some _ -> None
|
||||
|
|
@ -64,18 +165,22 @@ let private placeIn (file: string) (d: Diagnostics.Diagnostic) =
|
|||
if m.Success && sameFile m.Groups[1].Value then
|
||||
let line = max 0 (int m.Groups[2].Value - 1)
|
||||
|
||||
// The column counts from 1, as `Lexer.formatPos` writes it.
|
||||
if m.Groups[3].Success then
|
||||
let column = max 0 (int m.Groups[3].Value - 1)
|
||||
Some {| Line = line; Column = column; EndLine = line + 1; EndColumn = 0 |}
|
||||
|
||||
match markFrom line column with
|
||||
| Some mark -> Some mark
|
||||
| None -> Some {| Line = line; Column = column; EndLine = line + 1; EndColumn = 0 |}
|
||||
else
|
||||
Some(wholeLine line)
|
||||
else
|
||||
None
|
||||
|
||||
let private found (file: string) (d: Diagnostics.Diagnostic) : Found =
|
||||
let private found (text: string option) (file: string) (d: Diagnostics.Diagnostic) : Found =
|
||||
let isError = d.Severity = Diagnostics.Error
|
||||
|
||||
match placeIn file d with
|
||||
match placeIn text file d with
|
||||
| Some p ->
|
||||
{ Line = p.Line
|
||||
Column = p.Column
|
||||
|
|
@ -130,7 +235,14 @@ let file (buffers: Map<string, string>) (path: string) : Found list =
|
|||
let _, report = Diagnostics.captured Timing.report
|
||||
Rpc.log report
|
||||
|
||||
let collected = Diagnostics.snapshot () |> List.map (found path)
|
||||
// The text the check read, for what to mark at a column.
|
||||
let text =
|
||||
match buffers.TryFind(Path.GetFullPath path) with
|
||||
| Some t -> Some t
|
||||
| None when File.Exists path -> Some(File.ReadAllText path)
|
||||
| None -> None
|
||||
|
||||
let collected = Diagnostics.snapshot () |> List.map (found text path)
|
||||
|
||||
if status <> 0 && not (collected |> List.exists (fun f -> f.IsError)) then
|
||||
let message =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue