diff options
| author | 2026-10-15 10:11:56 +0330 | |
|---|---|---|
| committer | 2026-10-15 10:11:56 +0330 | |
| commit | e1e0a265e931637f452ac05bb7d912e07bcee574 (patch) | |
| tree | 60289bb1fc3d812dfc576f01e0f8a83091a495c1 | |
| parent | analyzer: check borrow returns of forward-called functions at the unit's end;... (diff) | |
lsp: borrow errors ranged on the use, with related information for where the owner ended and was declared; vscode client passes relatedInformation through
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | editors/vscode/README.md | 2 | ||||
| -rw-r--r-- | editors/vscode/client/extension.js | 7 | ||||
| -rw-r--r-- | src/compiler/analyzer.zc | 3 | ||||
| -rw-r--r-- | src/compiler/base.zc | 25 | ||||
| -rw-r--r-- | src/lsp/server.zc | 30 | ||||
| -rw-r--r-- | tests/drivers/borrow_diag_test.js | 73 | ||||
| -rw-r--r-- | tests/drivers/ext_cache_test.js | 13 | ||||
| -rwxr-xr-x | tests/test.sh | 4 |
9 files changed, 154 insertions, 6 deletions
@@ -96,8 +96,9 @@ hover shows declaration signatures and documentation. a top-level function, glob check mode records errors instead of printing them and continues after an error, up to 20 diagnostics per document. each record contains a line and token offset; the server maps these to utf-16 ranges. check mode also reports non-error findings as hints. - **diagnostics** on open, change and save, with an id echoed on every reply, `MethodNotFound` for unknown requests and `-32002` before `initialize`. +- **borrow diagnostics**: a use after its owner ended is the compiler's own error (so the editor and `make` agree), ranged on the conflicting use, with `relatedInformation` for where the owner ended (freed, moved, given to a `.d` parameter, ...) and where it was declared. a function called before its body is checked when the body has been read, and the error still lands on the use. - **definition and references** (`textDocument/definition`, `textDocument/references`): a local, parameter or field goes to its declaration by the offset the compiler recorded, and a function, global, constant, struct or type goes to where the compiler saw it defined, in an included file too (the file is found from the document's directory upward, then as written in the `include`). references lists the uses in the document, with the declaration when asked. built-in names navigate to their embedded source when one is available. -- **module completions**: after an imported namespace and a dot, completion lists that module's exported members with signatures and documentation; it does not add unrelated local or module names. module hovers and completions cover `rt`, `run`, `core`, `lsp`, `std` and `icon`. +- **module completions**: after an imported namespace and a dot, completion lists that module's exported members one level at a time (`std.` gives the groups such as `File` and `Io` and the plain members; `std.Io.` the members of `Io`) with signatures and documentation; it does not add unrelated local or module names. module hovers and completions cover `rt`, `run`, `core`, `lsp`, `std` and `icon`. - **import quick fixes**: compiler diagnostics for missing imports or unqualified exports offer a workspace edit. a bare name is looked up in every module: the runtime and compiler modules, loaded files, and the libraries under `src/lib` read from source even when they are not included, including static groups (`itoa` becomes `std.Text.itoa` with `include std;`). a misspelled name gets the closest defined name or module export (`putc` becomes `rt.put_c`). the edit adds the needed `include` without duplicating an existing import, qualifies bare exports, and preserves namespace declarations, comments, and crlf line endings. the editor client applies only edits for the current document version, while keeping protocol version epochs separate from editor document versions. - **semantic tokens** (`textDocument/semanticTokens/full`) in 12 types: keyword, type, function, variable, string, number, operator, comment, parameter, property, macro, enumMember. token kinds come from the compiler's symbol tables and declarations. check mode records each resolved name and source location, following the compiler's lookup order: locals and parameters precede globals, and built-ins are checked last. a local named `cap` is a variable wherever it is in scope, and the built-in `cap` only applies to uses outside that scope. - **formatting** (`textDocument/formatting`, and `zclsp --format < file.zc` on the command line): a pretty-printer in the spirit of clang-format with `ColumnLimit: 0`. it keeps the author's line breaks and sets the indent of every line (from braces, open parentheses and unfinished statements; `case` labels are indented one level inside their `switch` by default), the spaces after commas and around `=`, `==`, `&&` and the like, the blank lines between lines, and the final line break. it never moves a token to another line, and it leaves strings, comments, `asm` and `vm` blocks and everything after a backtick as written, so the tokens of a program are the same before and after. options come from a `.zcformat` file in the document's directory or the nearest directory above it, one `Key: value` per line (`#` starts a comment), then from the editor's tab size and `insertSpaces`, then from the defaults: `UseTab: true`, `IndentWidth: 4` (the spaces of a level when `UseTab` is false), `MaxEmptyLines: 1`, `ContinuationIndent: 1`, `NormalizeSpacing: true`, `CaseIndent: 1`, `FinalNewline: true`. `CaseIndent: 0` aligns each `case` label with its `switch`. `tests/drivers/format_test.js` formats every `.zc` file in the repository and checks that the tokens are unchanged and that formatting twice gives the same text. diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 24f27bb..506da42 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -2,7 +2,7 @@ this extension connects vscode to the zc language server. the server runs the compiler in check mode, so diagnostics and semantic highlighting reflect the language the compiler actually parses, including syntax introduced with parse words. -features include compiler diagnostics, import quick fixes, optimization hints, semantic highlighting, hover documentation (values, types, ownership and lifetimes, module include names, embedded declarations and assembly instruction descriptions), fuzzy completions with signatures and descriptions (consumed variables are crossed out and sorted last), go to definition and find references (also into included files), document formatting and immediate textmate highlighting while the server starts. module completion lists exported members after an imported namespace. import quick fixes preserve namespace headers, comments and crlf line endings, and the editor drops edits for stale document versions. hints are shown as blue information squiggles; `hints on`, `hints small` and `hints off` in the source change which ones appear. formatting reads a `.zcformat` file (`UseTab`, `IndentWidth`, `MaxEmptyLines`, `ContinuationIndent`, `NormalizeSpacing`, `CaseIndent`, `FinalNewline`) from the document's directory upward, and falls back to the editor's tab settings. by default, case labels are indented one level inside their switch; `CaseIndent: 0` aligns them with the switch. +features include compiler diagnostics, import quick fixes, optimization hints, semantic highlighting, hover documentation (values, types, ownership and lifetimes, module include names, embedded declarations and assembly instruction descriptions), fuzzy completions with signatures and descriptions (consumed variables are crossed out and sorted last), go to definition and find references (also into included files), document formatting and immediate textmate highlighting while the server starts. module completion goes one level at a time: `std.` lists the groups (`File`, `Io`, `Text`, ...) and plain members, `std.Io.` the members of that group. borrow errors are the compiler's own messages (the same check `zy` runs), marked on the conflicting use, with related information pointing at where the owner ended and where it was declared. import quick fixes preserve namespace headers, comments and crlf line endings, and the editor drops edits for stale document versions. hints are shown as blue information squiggles; `hints on`, `hints small` and `hints off` in the source change which ones appear. formatting reads a `.zcformat` file (`UseTab`, `IndentWidth`, `MaxEmptyLines`, `ContinuationIndent`, `NormalizeSpacing`, `CaseIndent`, `FinalNewline`) from the document's directory upward, and falls back to the editor's tab settings. by default, case labels are indented one level inside their switch; `CaseIndent: 0` aligns them with the switch. ## install diff --git a/editors/vscode/client/extension.js b/editors/vscode/client/extension.js index cd497d8..38c4c73 100644 --- a/editors/vscode/client/extension.js +++ b/editors/vscode/client/extension.js @@ -207,6 +207,13 @@ function showDiags(uri, raw) { : vscode.DiagnosticSeverity.Error; const diag = new vscode.Diagnostic(new vscode.Range(r.start.line, r.start.character, r.end.line, r.end.character), d.message, sev); diag.source = d.source || 'zc'; + // a borrow error names its owner: where it ended and where it was declared + if (Array.isArray(d.relatedInformation)) { + diag.relatedInformation = d.relatedInformation.filter(ri => ri && ri.location && ri.location.range).map(ri => { + const lr = ri.location.range; + return new vscode.DiagnosticRelatedInformation(new vscode.Location(vscode.Uri.parse(ri.location.uri || uri), new vscode.Range(lr.start.line, lr.start.character, lr.end.line, lr.end.character)), ri.message || ''); + }); + } return diag; }); stats.diagnostics += list.length; diff --git a/src/compiler/analyzer.zc b/src/compiler/analyzer.zc index 103f732..ff6a6c1 100644 --- a/src/compiler/analyzer.zc +++ b/src/compiler/analyzer.zc @@ -1002,9 +1002,10 @@ int lend_drop(int dep, int from) { // or untracked memory, 7 freed by reassigning it, 8 pushed into end_whom. int end_why; char* end_whom; int end_owner(int i) { - int j = lbase; word info = talloc(5); + int j = lbase; word info = talloc(6); lend_check(i); info[0] = lname[i]; info[1] = lpos[i]; info[2] = tpack(); info[3] = end_why; info[4] = end_whom; + info[5] = lreadpos[i]; if (!info[5]) info[5] = info[2]; // the owner's own token where it ended, for the editor end_why = 0; end_whom = 0; local_ended[i] = info[2]; local_end_info[i] = info; lstate[i] = S_DEAD; while (j < nloc) { diff --git a/src/compiler/base.zc b/src/compiler/base.zc index bfed062..7e4b5bc 100644 --- a/src/compiler/base.zc +++ b/src/compiler/base.zc @@ -415,6 +415,11 @@ int s_new_k, s_recycle_leaf; // server streams diagnostics and drops a check whose source has been replaced word chk_hook; int cmode, chkabort, chkfuel; word dgbuf, dggrow; int dgcap, dgpos, dgn, dgopen, dgprev, dgmode; +// related places of check-mode records, for the editor: DGREL_W words each, [the record's +// offset in dgbuf, the packed place where the owner ended, the packed place of the owner's +// declaration, how it ended (end_why), the owner's name]. reset by chk_begin. +const int DGREL_W = 5; +word dgrel; int dgreln, dgrelcap; // expand a diagnostic in progress, keeping its output offset within the buffer. int dg_expand() { dgbuf = chk_resize(dgbuf, dgpos + 5 + rt.outn, dgcap * 2); @@ -433,8 +438,22 @@ int dg_expand() { int probe_off = -1, probe_has, probe_n, probe_cap, probe_static; char** probe_name; word probe_kind, probe_state, probe_type, probe_decl, probe_mem; char* curinc; int incdepth, dgdrop; int incplace; // how many includes deep the compile is; a record to throw away; the place of the outermost include +// an open record's range becomes the packed place p (the use itself, not the token after it). +int dg_place(int p) { + if (!cmode || !dgopen || incdepth > 0 || !p || (p & 0xFFFFFF) != tline) return 0; + dgbuf[dgpos + 1] = p & 0xFFFFFF; dgbuf[dgpos + 2] = ((p >> 40) & 0x1FFFFF) - 1; dgbuf[dgpos + 3] = (p >> 24) & 0xFFFF; + return 0; +} +int dg_related(int end_place, int decl_place, int why, char* owner) { + if (!cmode || !dgopen || dgdrop || incdepth > 0) return 0; + if (dgreln == dgrelcap) { int cap = dgrelcap * 2; if (!cap) cap = 16; dgrel = chk_resize(dgrel, dgreln * DGREL_W, cap * DGREL_W); dgrelcap = cap; } + word e = dgrel + dgreln * DGREL_W; + e[0] = dgpos; e[1] = end_place; e[2] = decl_place; e[3] = why; e[4] = owner; dgreln++; + return 0; +} int dg_close() { if (!dgopen) return 0; + if (dgdrop && dgreln && dgrel[(dgreln - 1) * DGREL_W] == dgpos) dgreln--; // its record is thrown away if (!dgdrop) { dgbuf[dgpos + 4] = rt.outn; dgpos = dgpos + 5 + rt.outn; dgn++; } dgdrop = 0; dgopen = 0; rt.outmode = dgmode; rt.outgrow = dggrow; @@ -470,7 +489,7 @@ word[256] dgseen_initial; word dgseen = dgseen_initial; int dgseen_cap = 256, dg // start and stop a check run. chk_begin points the diagnostics at a buffer // the caller owns; chk_end flushes any open record and restores output. int chk_begin(int buf, int cap) { - cmode = 1; chkabort = 0; chkfuel = 0; dgbuf = buf; dgcap = cap; dgpos = 0; dgn = 0; dgopen = 0; + cmode = 1; chkabort = 0; chkfuel = 0; dgbuf = buf; dgcap = cap; dgpos = 0; dgn = 0; dgopen = 0; dgreln = 0; nerr = 0; panic = 0; dgmode = rt.outmode; rt.fill_w(dgseen, 0, dgseen_cap); dgseen_n = 0; return 0; } @@ -963,7 +982,7 @@ int cerr(char* msg) { // "r: used after it was given away; r borrows t, and t was freed on line 5 (t declared on line 4)" int ended_message(char* use, word info, char* root, int cause) { char* owner = root; int why = 0; char* whom = 0; - if (info) { owner = info[0]; why = info[3]; whom = info[4]; } + if (info) { owner = info[0]; why = info[3]; whom = info[4]; dg_related(info[5], info[1], why, owner); } rt.put_s(use); rt.put_s(": used after it was given away; "); if (!rt.eq_w(use, owner)) { rt.put_s(use); rt.put_s(" borrows "); rt.put_s(owner); rt.put_s(", and "); } rt.put_s(owner); @@ -989,7 +1008,7 @@ int cerrn(int i, char* msg) { if (!info && local_end_info) info = local_end_info[root]; if (info) cause = info[2]; else if (local_ended) cause = local_ended[root]; } - if (cause && !panic && !dry) { errat(); ended_message(lname[i], info, lname[root], cause); errend(); } + if (cause && !panic && !dry) { errat(); dg_place(lreadpos[i]); ended_message(lname[i], info, lname[root], cause); errend(); } else serrn(lname[i], msg); } return 0; diff --git a/src/lsp/server.zc b/src/lsp/server.zc index 6f4ce67..65881a9 100644 --- a/src/lsp/server.zc +++ b/src/lsp/server.zc @@ -258,6 +258,35 @@ int wpos(int off) { wc('}'); return 0; } +// a packed compiler place (tpack) as an lsp location in document gd. +int wplace(int p) { + int o = ((p >> 40) & 0x1FFFFF) - 1, n = (p >> 24) & 0xFFFF; + if (o < 0) o = 0; if (o > plen) o = plen; + if (n < 1) n = 1; if (o + n > plen) n = plen - o; + ws("{\"uri\":"); wjs(duri[gd], dulen[gd]); ws(",\"range\":{\"start\":"); wpos(o); ws(",\"end\":"); wpos(o + n); ws("}}"); + return 0; +} +// the related places the compiler recorded for the record at offset at (run.dg_related): +// where the owner ended and where it was declared, as relatedInformation. +int diag_related(int at) { + int k = 0, why; word e; char* what; + while (k < run.dgreln) { + e = run.dgrel + k * run.DGREL_W; + if (e[0] == at) { + why = e[3]; what = " ended here"; + if (why == 1) what = " was freed here"; else if (why == 2) what = " was given to a .d parameter here"; + else if (why == 3) what = " was moved here"; else if (why == 4) what = " was stored into a container here"; + else if (why == 5) what = " was returned here"; else if (why == 6) what = " was stored in a global here"; + else if (why == 7) what = " was freed by this reassignment"; else if (why == 8) what = " was pushed into an array here"; + ws(",\"relatedInformation\":[{\"location\":"); wplace(e[1]); ws(",\"message\":\"owner "); ws(e[4]); ws(what); ws("\"}"); + if (e[2]) { ws(",{\"location\":"); wplace(e[2]); ws(",\"message\":\"owner "); ws(e[4]); ws(" is declared here\"}"); } + wc(']'); + return 0; + } + k++; + } + return 0; +} // writes the publishDiagnostics message for document gd from dgb. each record // becomes a range (the whole line when the compiler only knew the line), a // severity and its message. @@ -295,6 +324,7 @@ int gen_diag() { ws("{\"range\":{\"start\":"); wpos(o); ws(",\"end\":"); wpos(e); ws("},\"severity\":"); wn(sev); ws(",\"source\":\"zc\",\"message\":"); wjs(dgb + at + 5, n); + diag_related(at); wc('}'); at = at + 5 + n; } diff --git a/tests/drivers/borrow_diag_test.js b/tests/drivers/borrow_diag_test.js new file mode 100644 index 0000000..93a7561 --- /dev/null +++ b/tests/drivers/borrow_diag_test.js @@ -0,0 +1,73 @@ +// borrow diagnostics in the editor: the server publishes the compiler's own messages +// (the same check `zy` runs), with the range of the conflicting use and related +// information for where the owner ended and where it was declared. +// usage: node tests/drivers/borrow_diag_test.js path/to/zclsp [server arguments] +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; +const assert = require('assert'); +const cp = require('child_process'); +const path = require('path'); +const uri = 'file://' + path.resolve('tests/drivers/borrow-diag-fixture.zc'); +const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); }; +function diagnose(text) { + const msgs = [ + { id: 1, method: 'initialize', params: {} }, + { method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } }, + { id: 2, method: 'shutdown' }, + { method: 'exit' } + ].map(m => ({ jsonrpc: '2.0', ...m })); + const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); + assert.strictEqual(r.status, 0, String(r.stderr)); + let b = r.stdout, last = null; + while (b.length) { + const e = b.indexOf('\r\n\r\n'); assert.ok(e >= 0); + const n = Number(/Content-Length:\s*(\d+)/i.exec(b.subarray(0, e).toString())[1]); + const m = JSON.parse(b.subarray(e + 4, e + 4 + n)); b = b.subarray(e + 4 + n); + if (m.method === 'textDocument/publishDiagnostics') last = m.params.diagnostics; + } + assert.ok(last, 'no diagnostics published'); + return last; +} +function zy(text) { + const zyBin = process.env.ZY || path.join(path.dirname(process.argv[2]), 'zy'); + const r = cp.spawnSync(zyBin, [], { input: text, timeout: 30000 * TIMEOUT_SCALE }); + const line = String(r.stdout).split('\n')[0]; + return line.replace(/^zc: line \d+: /, ''); +} +const at = (text, needle, nth = 0) => { let o = -1; for (let k = 0; k <= nth; k++) o = text.indexOf(needle, o + 1); const before = text.slice(0, o); return { line: before.split('\n').length - 1, character: o - before.lastIndexOf('\n') - 1 }; }; +let checks = 0; +function same(text, label) { + const d = diagnose(text).filter(x => x.severity === 1); + assert.ok(d.length >= 1, label + ': no error'); + const fromZy = zy(text); + assert.ok(fromZy && d.some(x => x.message === fromZy), `${label}: editor and zy disagree: [${d.map(x => x.message)}] vs [${fromZy}]`); + checks++; + return d.find(x => x.message === fromZy); +} +// a borrow used after its owner is freed +let text = 'include rt;\nfn main() {\n\tchar* t = rt.new(4); t[0] = 65;\n\tchar* r = t + 1;\n\trt.free(t);\n\treturn r[0];\n}\n'; +let d = same(text, 'freed owner'); +assert.match(d.message, /r borrows t, and t was freed/); +assert.deepStrictEqual(d.range.start, at(text, 'r[0]'), 'the range is the conflicting use'); +assert.ok(d.relatedInformation && d.relatedInformation.length === 2, JSON.stringify(d)); +assert.strictEqual(d.relatedInformation[0].message, 'owner t was freed here'); +assert.deepStrictEqual(d.relatedInformation[0].location.range.start, at(text, 't);')); +assert.strictEqual(d.relatedInformation[0].location.uri, uri); +assert.strictEqual(d.relatedInformation[1].message, 'owner t is declared here'); +assert.deepStrictEqual(d.relatedInformation[1].location.range.start, at(text, 't =')); +// the owner itself, given to a .d parameter +text = 'include rt;\nfn take(.d p) { return p[0]; }\nfn main() { let x = rt.new(3); take(x); return x[0]; }\n'; +d = same(text, '.d parameter'); +assert.match(d.message, /x was given to take's \.d parameter/); +assert.strictEqual(d.relatedInformation[0].message, 'owner x was given to a .d parameter here'); +assert.deepStrictEqual(d.relatedInformation[0].location.range.start, at(text, 'x)')); +// a function called before its body, reported at the end of the unit at the use +text = 'include rt;\nfn main() {\n\tchar* t = rt.new(8); t[0] = 32; t[1] = 0;\n\tlet r = skip(t); rt.free(t); rt.put_c(r[0]);\n\treturn 0;\n}\nchar* skip(char* s) { return s + 1; }\n'; +d = same(text, 'forward call'); +assert.match(d.message, /skip, defined after this call, returns a borrow/); +assert.deepStrictEqual(d.range.start, at(text, 'r[0]')); +assert.strictEqual(d.relatedInformation[0].message, 'owner t was freed here'); +// a borrows clause checked against its body +text = 'include rt;\nchar* first(char* a, char* b) borrows a;\nchar* first(char* a, char* b) { return b; }\nfn main() { return 0; }\n'; +d = same(text, 'borrows clause'); +assert.match(d.message, /returns a borrow of b/); +console.log(`ok borrow diagnostics: ${checks} cases match zy, with ranges and related owner places`); diff --git a/tests/drivers/ext_cache_test.js b/tests/drivers/ext_cache_test.js index 93caf94..9bd3e3a 100644 --- a/tests/drivers/ext_cache_test.js +++ b/tests/drivers/ext_cache_test.js @@ -31,6 +31,8 @@ const stub = { EventEmitter: class { constructor() { this.event = () => ({ dispose() {} }); } fire() {} }, Diagnostic: class { constructor(range, message, severity) { Object.assign(this, { range, message, severity }); } }, Range: class { constructor(a, b, c, d) { Object.assign(this, { start: { line: a, character: b }, end: { line: c, character: d } }); } }, + Location: class { constructor(uri, range) { Object.assign(this, { uri, range }); } }, + DiagnosticRelatedInformation: class { constructor(location, message) { Object.assign(this, { location, message }); } }, DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 }, SemanticTokensLegend: class { constructor(t, m) { this.tokenTypes = t; this.tokenModifiers = m; } }, SemanticTokensBuilder: class { constructor() {} push() {} build() { return null; } }, @@ -177,6 +179,17 @@ const mainUri = 'file://' + main; const expiredList = await world.completion.provideCompletionItems(expiredDoc, { line: 3, character: 1 }, { isCancellationRequested: false }); const expiredItem = expiredList && expiredList.items.find(c => c.label === 'used'); check('completion maps consumed lifetimes to the editor strikethrough tag', expiredItem && expiredItem.tags?.[0] === stub.CompletionItemTag.Deprecated && /lifetime ended/.test(expiredItem.documentation?.value)); + // a borrow error keeps the server's related information: where the owner ended and where it was declared + const borrowPath = path.join(root, 'borrow.zc'); + const borrowDoc = doc(borrowPath, 'include rt;\nfn main() {\n\tchar* t = rt.new(4); t[0] = 65;\n\tchar* r = t + 1;\n\trt.free(t);\n\treturn r[0];\n}\n'); + fs.writeFileSync(borrowPath, borrowDoc.text); + world.textDocuments.push(borrowDoc); + if (world.listeners.onDidOpenTextDocument) world.listeners.onDidOpenTextDocument(borrowDoc); + await until(() => world.sets.some(s => s.uri === 'file://' + borrowPath && s.list.length)); + const borrowSet = world.sets.filter(s => s.uri === 'file://' + borrowPath).pop(); + const borrowDiag = borrowSet && borrowSet.list.find(d => /r borrows t/.test(d.message)); + const related = borrowDiag && borrowDiag.relatedInformation; + check('borrow diagnostics pass related information through to the editor', related && related.length === 2 && related[0] instanceof stub.DiagnosticRelatedInformation && related[0].message === 'owner t was freed here' && related[0].location.range.start.line === 4 && related[1].message === 'owner t is declared here', JSON.stringify(borrowSet && borrowSet.list)); const actionDoc = doc(path.join(root, 'imports.zc'), 'fn main() { rt.put_n(7); }'); const actions = await world.actions.provideCodeActions(actionDoc, new stub.Range(0, 15, 0, 15), {}, { isCancellationRequested: false }); check('quick fixes reach the editor as workspace edits', actions.some(a => a.kind === 'quickfix' && a.edit.edits.some(e => e.newText === 'include rt;\n'))); diff --git a/tests/test.sh b/tests/test.sh index d8ad0fb..6d8453e 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -637,6 +637,10 @@ s_lsp() { node tests/drivers/completion_test.js $lsp > $T/completion.out 2>&1; r=$? grep -v '^ok' $T/completion.out chk $r 0 "lsp: fuzzy completions, signatures, scope and expired lifetimes" + $CC -pthread -O2 -w -fwrapv -DJTYPE=int src/core/core.c src/core/io_linux.c $GI -o $T/zy + ZY=$T/zy node tests/drivers/borrow_diag_test.js $lsp > $T/borrowdiag.out 2>&1; r=$? + grep -v '^ok' $T/borrowdiag.out + chk $r 0 "lsp: borrow errors are zy's own messages, with the use's range and the owner as related information" node tests/drivers/analysis_cache_test.js $lsp > $T/analysiscache.out 2>&1; r=$? grep -v '^ok' $T/analysiscache.out chk $r 0 "lsp: analysis cache reuse and invalidation" |
