kopscript 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLM.md +10 -1
- package/README.md +12 -4
- package/dist/checker.js +103 -13
- package/dist/codegen.js +21 -2
- package/dist/diagnostics.js +14 -5
- package/dist/modules.js +12 -1
- package/dist/parser.js +1 -1
- package/dist/suggest.js +44 -0
- package/package.json +1 -1
package/LLM.md
CHANGED
|
@@ -23,9 +23,18 @@ ks build|check <file.ks> --json # single JSON object on stdout instead of huma
|
|
|
23
23
|
`--json` output shape (also what a tool/agent should parse instead of scraping text):
|
|
24
24
|
|
|
25
25
|
```json
|
|
26
|
-
{ "success": false, "diagnostics": [{ "code": "
|
|
26
|
+
{ "success": false, "diagnostics": [{ "code": "KS4083", "severity": "error", "message": "Class 'Widget' has no member 'Vlaue'", "line": 3, "col": 13, "fix": "Did you mean 'Value'?", "file": "/abs/path.ks" }], "written": [] }
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
`fix` is present only when the checker/parser found something concrete enough to say beyond
|
|
30
|
+
the message itself — a real "did you mean 'X'?" against an actually-declared name close
|
|
31
|
+
enough to be a plausible typo (unknown member/function/identifier/type — `KS4083`/`KS4062`/
|
|
32
|
+
`KS4048`/`KS4008`/`KS4076`/`KS4077`), or a precise, always-accurate pointer for a handful of
|
|
33
|
+
other high-frequency errors (a missing `await` on `KS4059`, `match` also accepting an enum
|
|
34
|
+
subject on `KS4046`, a stray type-parameter list on `KS2023`, an invalid `match` pattern on
|
|
35
|
+
`KS4088`). Absent entirely (no empty string) when nothing that specific applies — most
|
|
36
|
+
diagnostics still don't have one, and that's expected, not a gap to fill in for every code.
|
|
37
|
+
|
|
29
38
|
`written` is the absolute paths actually written (`build` only, and only on success — a
|
|
30
39
|
failed `build` writes nothing; `.js.map` paths aren't listed separately, one is written next
|
|
31
40
|
to each `.js` path in `written`). Exit code is `0` iff `success` is `true`. A missing entry
|
package/README.md
CHANGED
|
@@ -873,12 +873,20 @@ programmatically instead of scraping formatted text:
|
|
|
873
873
|
{
|
|
874
874
|
"success": false,
|
|
875
875
|
"diagnostics": [
|
|
876
|
-
{ "code": "
|
|
876
|
+
{ "code": "KS4083", "severity": "error", "message": "Class 'Widget' has no member 'Vlaue'", "line": 3, "col": 13, "fix": "Did you mean 'Value'?", "file": "/abs/path/to/file.ks" }
|
|
877
877
|
],
|
|
878
878
|
"written": []
|
|
879
879
|
}
|
|
880
880
|
```
|
|
881
881
|
|
|
882
|
+
Some diagnostics also carry a `fix` field — a real "did you mean 'X'?" against something
|
|
883
|
+
actually declared, for the unknown-member/function/identifier/type family of errors, or a
|
|
884
|
+
precise next step for a handful of other common ones (a missing `await`, `match` also
|
|
885
|
+
accepting an enum subject, a stray type-parameter list). Not every diagnostic has one —
|
|
886
|
+
absent entirely, not an empty string, when nothing that specific applies — but where it's
|
|
887
|
+
present, it's meant to be actionable directly, by a human or an AI agent, without needing to
|
|
888
|
+
re-derive it from the message alone.
|
|
889
|
+
|
|
882
890
|
`written` lists the absolute paths of every `.js` file actually written (always empty for
|
|
883
891
|
`check`, and for `build` on failure — nothing is written unless the whole graph is
|
|
884
892
|
error-free). A missing entry file reports `{ "success": false, "diagnostics": [],
|
|
@@ -890,9 +898,9 @@ Every diagnostic carries a stable `code` (`KS` + a number) alongside its human-r
|
|
|
890
898
|
of parsing prose that can be reworded between versions. Codes are grouped by pipeline stage
|
|
891
899
|
and never reused once assigned: `KS1xxx` lexer, `KS2xxx` parser, `KS3xxx` module/`using`
|
|
892
900
|
resolution, `KS4xxx` the checker (by far the largest category — most real type errors live
|
|
893
|
-
here), `KS5xxx` templates. There's no generated reference doc mapping every code to an
|
|
894
|
-
explanation
|
|
895
|
-
matching, not
|
|
901
|
+
here), `KS5xxx` templates. There's still no generated reference doc mapping every code to an
|
|
902
|
+
explanation — `message` (plus `fix`, when present — see above) is the explanation; `code` is
|
|
903
|
+
for matching reliably across wording changes, not for looking up a separate page.
|
|
896
904
|
|
|
897
905
|
**Source maps**: `build` writes a real source-map v3 `<file>.js.map` alongside every
|
|
898
906
|
`<file>.js`, with the original `.ks` source embedded (`sourcesContent`) so a deployed app
|
package/dist/checker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { closestMatch } from "./suggest.js";
|
|
3
4
|
import * as T from "./types.js";
|
|
4
5
|
export function emptyModuleExports() {
|
|
5
6
|
return { namedTypes: new Map(), classes: new Map(), interfaces: new Map(), enums: new Map(), functions: new Map(), externValues: new Map() };
|
|
@@ -40,6 +41,14 @@ class Scope {
|
|
|
40
41
|
child() {
|
|
41
42
|
return new Scope(this);
|
|
42
43
|
}
|
|
44
|
+
// Every local/param name visible from here, walking up through every
|
|
45
|
+
// enclosing scope — used only to build "did you mean 'X'?" candidates for
|
|
46
|
+
// an undefined-identifier error (KS4048), never for real name resolution
|
|
47
|
+
// (resolve() above already does that correctly, including shadowing).
|
|
48
|
+
allNames() {
|
|
49
|
+
const own = [...this.vars.keys()];
|
|
50
|
+
return this.parent ? [...own, ...this.parent.allNames()] : own;
|
|
51
|
+
}
|
|
43
52
|
}
|
|
44
53
|
export class Checker {
|
|
45
54
|
recordHover(line, col, text) {
|
|
@@ -389,7 +398,9 @@ export class Checker {
|
|
|
389
398
|
this.diagnostics.error("KS4007", `Generic type '${name}' requires ${this.describeArity(declaredParams)} (e.g. '${name}<${declaredParams.join(", ")}>')`, line, col);
|
|
390
399
|
}
|
|
391
400
|
else {
|
|
392
|
-
this.
|
|
401
|
+
const candidates = [...this.namedTypes.keys(), "number", "string", "bool", "void"];
|
|
402
|
+
const suggestion = closestMatch(name, candidates);
|
|
403
|
+
this.diagnostics.error("KS4008", `Unknown type '${name}'`, line, col, suggestion ? `Did you mean '${suggestion}'?` : undefined);
|
|
393
404
|
}
|
|
394
405
|
return T.UNKNOWN;
|
|
395
406
|
}
|
|
@@ -1071,6 +1082,35 @@ export class Checker {
|
|
|
1071
1082
|
}
|
|
1072
1083
|
return null;
|
|
1073
1084
|
}
|
|
1085
|
+
// Every instance field/method name declared anywhere in className's own
|
|
1086
|
+
// superclass chain — used only to build "did you mean 'X'?" candidates
|
|
1087
|
+
// for an unknown-member error (KS4083), never for real member resolution
|
|
1088
|
+
// (lookupField/lookupMethod above already do that correctly).
|
|
1089
|
+
allInstanceMemberNames(className) {
|
|
1090
|
+
const names = [];
|
|
1091
|
+
let current = className;
|
|
1092
|
+
while (current) {
|
|
1093
|
+
const info = this.classes.get(current);
|
|
1094
|
+
if (!info)
|
|
1095
|
+
break;
|
|
1096
|
+
names.push(...info.fields.keys(), ...info.methods.keys());
|
|
1097
|
+
current = info.superclass;
|
|
1098
|
+
}
|
|
1099
|
+
return names;
|
|
1100
|
+
}
|
|
1101
|
+
// Same, for static members (KS4077's own candidates).
|
|
1102
|
+
allStaticMemberNames(className) {
|
|
1103
|
+
const names = [];
|
|
1104
|
+
let current = className;
|
|
1105
|
+
while (current) {
|
|
1106
|
+
const info = this.classes.get(current);
|
|
1107
|
+
if (!info)
|
|
1108
|
+
break;
|
|
1109
|
+
names.push(...info.staticFields.keys(), ...info.staticMethods.keys());
|
|
1110
|
+
current = info.superclass;
|
|
1111
|
+
}
|
|
1112
|
+
return names;
|
|
1113
|
+
}
|
|
1074
1114
|
// Enforces C#-style private/protected/public access from the current class context.
|
|
1075
1115
|
checkAccessibility(visibility, owner, ctx, memberName, line, col) {
|
|
1076
1116
|
if (visibility === "public")
|
|
@@ -1384,7 +1424,14 @@ export class Checker {
|
|
|
1384
1424
|
}
|
|
1385
1425
|
expectType(actual, expected, line, col, context) {
|
|
1386
1426
|
if (!T.typesEqual(actual, expected)) {
|
|
1387
|
-
|
|
1427
|
+
// The one context-specific hint worth giving here: a match subject
|
|
1428
|
+
// that's neither string nor enum most likely means the author didn't
|
|
1429
|
+
// know match accepts an enum too (see LLM.md's "match expression").
|
|
1430
|
+
// Every OTHER expectType call site (if-condition, binary-op operand,
|
|
1431
|
+
// ...) doesn't have an equally specific, generically-true next step,
|
|
1432
|
+
// so it gets no fix rather than a vague one.
|
|
1433
|
+
const fix = context === "match subject" ? `match also accepts an enum subject — see LLM.md's "match expression" section.` : undefined;
|
|
1434
|
+
this.diagnostics.error("KS4046", `Expected type '${T.typeToString(expected)}' for ${context}, got '${T.typeToString(actual)}'`, line, col, fix);
|
|
1388
1435
|
}
|
|
1389
1436
|
}
|
|
1390
1437
|
// Recognizes `name != null` / `name == null` (either operand order) as a
|
|
@@ -1478,7 +1525,23 @@ export class Checker {
|
|
|
1478
1525
|
const fnInfo = this.functions.get(expr.name);
|
|
1479
1526
|
if (fnInfo)
|
|
1480
1527
|
return T.functionType(fnInfo.params, fnInfo.returnType);
|
|
1481
|
-
|
|
1528
|
+
{
|
|
1529
|
+
const candidates = [...scope.allNames(), ...this.functions.keys(), ...this.externValues.keys()];
|
|
1530
|
+
const suggestion = closestMatch(expr.name, candidates);
|
|
1531
|
+
// No implicit `this` (see LLM.md's "Common mistakes") is a real,
|
|
1532
|
+
// common, PRECISELY detectable case of this exact error: the name
|
|
1533
|
+
// is undefined as a bare identifier, but it IS a real member of
|
|
1534
|
+
// the enclosing class — so it's always accurate to point at
|
|
1535
|
+
// `this.${name}`, never a guess the way the Levenshtein
|
|
1536
|
+
// suggestion above already handles unrelated-typo cases.
|
|
1537
|
+
const isOwnMember = ctx.currentClass ? this.allInstanceMemberNames(ctx.currentClass.name).includes(expr.name) : false;
|
|
1538
|
+
const fix = suggestion
|
|
1539
|
+
? `Did you mean '${suggestion}'?`
|
|
1540
|
+
: isOwnMember
|
|
1541
|
+
? `No implicit 'this' — did you mean 'this.${expr.name}'?`
|
|
1542
|
+
: undefined;
|
|
1543
|
+
this.diagnostics.error("KS4048", `Undefined identifier '${expr.name}'`, expr.line, expr.col, fix);
|
|
1544
|
+
}
|
|
1482
1545
|
return T.UNKNOWN;
|
|
1483
1546
|
}
|
|
1484
1547
|
case "ThisExpr": {
|
|
@@ -1665,7 +1728,12 @@ export class Checker {
|
|
|
1665
1728
|
return T.VOID;
|
|
1666
1729
|
}
|
|
1667
1730
|
if (expectedReturnType.kind !== "unknown" && !this.isAssignableType(actual, expectedReturnType)) {
|
|
1668
|
-
|
|
1731
|
+
// A `task`-typed body where anything else was expected is very often
|
|
1732
|
+
// a missing `await`, not a genuinely wrong return type — precise and
|
|
1733
|
+
// always accurate to say so specifically, unlike a generic "check the
|
|
1734
|
+
// types" hint that wouldn't be worth giving.
|
|
1735
|
+
const fix = actual.kind === "task" ? "Did you forget 'await'?" : undefined;
|
|
1736
|
+
this.diagnostics.error("KS4059", `Lambda body has type '${T.typeToString(actual)}', expected '${T.typeToString(expectedReturnType)}'`, expr.line, expr.col, fix);
|
|
1669
1737
|
}
|
|
1670
1738
|
return actual;
|
|
1671
1739
|
}
|
|
@@ -1722,7 +1790,20 @@ export class Checker {
|
|
|
1722
1790
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1723
1791
|
return T.VOID;
|
|
1724
1792
|
}
|
|
1725
|
-
|
|
1793
|
+
{
|
|
1794
|
+
// Same "no implicit this" mistake KS4048 checks for (see its own
|
|
1795
|
+
// comment) — `Increment()` inside a method never resolves to
|
|
1796
|
+
// `this.Increment()`, even when Increment really is a method on
|
|
1797
|
+
// the enclosing class.
|
|
1798
|
+
const isOwnMethod = ctx.currentClass ? this.allInstanceMemberNames(ctx.currentClass.name).includes(expr.callee.name) : false;
|
|
1799
|
+
const suggestion = isOwnMethod ? null : closestMatch(expr.callee.name, [...this.functions.keys()]);
|
|
1800
|
+
const fix = isOwnMethod
|
|
1801
|
+
? `No implicit 'this' — did you mean 'this.${expr.callee.name}(...)'?`
|
|
1802
|
+
: suggestion
|
|
1803
|
+
? `Did you mean '${suggestion}'?`
|
|
1804
|
+
: undefined;
|
|
1805
|
+
this.diagnostics.error("KS4062", `Undefined function '${expr.callee.name}'`, expr.line, expr.col, fix);
|
|
1806
|
+
}
|
|
1726
1807
|
expr.args.forEach((a) => this.checkExpression(a, scope, ctx));
|
|
1727
1808
|
return T.UNKNOWN;
|
|
1728
1809
|
}
|
|
@@ -2054,7 +2135,8 @@ export class Checker {
|
|
|
2054
2135
|
this.recordHover(expr.object.line, expr.object.col, `enum ${objName}`);
|
|
2055
2136
|
const enumInfo = this.enums.get(objName);
|
|
2056
2137
|
if (!enumInfo.members.has(expr.property)) {
|
|
2057
|
-
|
|
2138
|
+
const suggestion = closestMatch(expr.property, [...enumInfo.members.keys()]);
|
|
2139
|
+
this.diagnostics.error("KS4076", `Enum '${objName}' has no member '${expr.property}'`, expr.line, expr.col, suggestion ? `Did you mean '${objName}.${suggestion}'?` : undefined);
|
|
2058
2140
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
2059
2141
|
}
|
|
2060
2142
|
return { type: T.enumType(objName), methodInfo: null };
|
|
@@ -2071,7 +2153,10 @@ export class Checker {
|
|
|
2071
2153
|
this.checkAccessibility(method.info.visibility, method.owner, ctx, expr.property, expr.line, expr.col);
|
|
2072
2154
|
return { type: method.info.returnType, methodInfo: method.info };
|
|
2073
2155
|
}
|
|
2074
|
-
|
|
2156
|
+
{
|
|
2157
|
+
const suggestion = closestMatch(expr.property, this.allStaticMemberNames(objName));
|
|
2158
|
+
this.diagnostics.error("KS4077", `Class '${objName}' has no static member '${expr.property}'`, expr.line, expr.col, suggestion ? `Did you mean '${objName}.${suggestion}'?` : undefined);
|
|
2159
|
+
}
|
|
2075
2160
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
2076
2161
|
}
|
|
2077
2162
|
}
|
|
@@ -2159,7 +2244,12 @@ export class Checker {
|
|
|
2159
2244
|
};
|
|
2160
2245
|
return { type: info.returnType, methodInfo: info };
|
|
2161
2246
|
}
|
|
2162
|
-
|
|
2247
|
+
{
|
|
2248
|
+
const suggestion = closestMatch(expr.property, this.allInstanceMemberNames(objectType.name));
|
|
2249
|
+
this.diagnostics.error("KS4083", `Class '${objectType.name}' has no member '${expr.property}'`, expr.line, expr.col, suggestion
|
|
2250
|
+
? `Did you mean '${suggestion}'?`
|
|
2251
|
+
: `No similar member found on '${objectType.name}' — if this should exist, check its class (or extern binding, if '${objectType.name}' describes external JS) actually declares it.`);
|
|
2252
|
+
}
|
|
2163
2253
|
return { type: T.UNKNOWN, methodInfo: null };
|
|
2164
2254
|
}
|
|
2165
2255
|
if (objectType.kind === "interface") {
|
|
@@ -2243,11 +2333,11 @@ export class Checker {
|
|
|
2243
2333
|
// valueType === "unknown" means checkExpression already
|
|
2244
2334
|
// reported its own error (e.g. "Enum has no member X") —
|
|
2245
2335
|
// don't pile a second, less specific one on top of it.
|
|
2246
|
-
this.reportInvalidMatchPattern(`match pattern must be a member of enum '${enumName}' (e.g. '${enumName}.SomeMember')`, value.line, value.col);
|
|
2336
|
+
this.reportInvalidMatchPattern(`match pattern must be a member of enum '${enumName}' (e.g. '${enumName}.SomeMember')`, value.line, value.col, `See LLM.md's "match expression" section for enum-pattern syntax.`);
|
|
2247
2337
|
}
|
|
2248
2338
|
}
|
|
2249
2339
|
else if (value.kind !== "StringLiteral") {
|
|
2250
|
-
this.reportInvalidMatchPattern(`match patterns must be string literals`, value.line, value.col);
|
|
2340
|
+
this.reportInvalidMatchPattern(`match patterns must be string literals`, value.line, value.col, `A non-string, non-enum match subject isn't supported at all — see LLM.md's "match expression" section.`);
|
|
2251
2341
|
}
|
|
2252
2342
|
else {
|
|
2253
2343
|
this.checkExpression(value, scope, ctx);
|
|
@@ -2255,7 +2345,7 @@ export class Checker {
|
|
|
2255
2345
|
}
|
|
2256
2346
|
}
|
|
2257
2347
|
else if (arm.pattern.kind === "RegexPattern" && isEnumSubject) {
|
|
2258
|
-
this.reportInvalidMatchPattern(`a regex pattern is not valid for a match over enum '${enumName}' — enum patterns must name a member`, arm.line, arm.col);
|
|
2348
|
+
this.reportInvalidMatchPattern(`a regex pattern is not valid for a match over enum '${enumName}' — enum patterns must name a member`, arm.line, arm.col, `Use '${enumName}.SomeMember' patterns instead — see LLM.md's "match expression" section.`);
|
|
2259
2349
|
}
|
|
2260
2350
|
// WildcardPattern (and RegexPattern against a string subject) need no
|
|
2261
2351
|
// further checking here.
|
|
@@ -2282,7 +2372,7 @@ export class Checker {
|
|
|
2282
2372
|
// subject's kind (not a string literal against a string subject, not a
|
|
2283
2373
|
// same-enum member reference against an enum subject, or a regex pattern
|
|
2284
2374
|
// against an enum subject at all).
|
|
2285
|
-
reportInvalidMatchPattern(message, line, col) {
|
|
2286
|
-
this.diagnostics.error("KS4088", message, line, col);
|
|
2375
|
+
reportInvalidMatchPattern(message, line, col, fix) {
|
|
2376
|
+
this.diagnostics.error("KS4088", message, line, col, fix);
|
|
2287
2377
|
}
|
|
2288
2378
|
}
|
package/dist/codegen.js
CHANGED
|
@@ -98,8 +98,27 @@ export class CodeGenerator {
|
|
|
98
98
|
// `public` into scope without naming it explicitly. `sourceFileName`/
|
|
99
99
|
// `sourceText` are for the emitted source map only (both optional so every
|
|
100
100
|
// existing test call site keeps compiling unchanged).
|
|
101
|
-
generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = ""
|
|
102
|
-
|
|
101
|
+
generate(program, usingExports = new Map(), rawContents = new Map(), sourceFileName = "source.ks", sourceText = "",
|
|
102
|
+
// Interface names visible via a `using` this module doesn't declare
|
|
103
|
+
// itself — separate from `usingExports` above, which deliberately
|
|
104
|
+
// excludes interfaces (they're compile-time only and never produce a
|
|
105
|
+
// JS import). Without this, a class implementing an interface declared
|
|
106
|
+
// in a DIFFERENT file (e.g. `class Component : Flushable` where
|
|
107
|
+
// `Flushable` lives in vdom.ks) has that interface name missing from
|
|
108
|
+
// `interfaceNames` entirely, so genClass's own "only a non-interface
|
|
109
|
+
// base list entry becomes `extends`" check can't tell it apart from a
|
|
110
|
+
// real (missing) superclass — it gets emitted as `extends Flushable`,
|
|
111
|
+
// a real runtime reference to a name nothing ever imports, throwing
|
|
112
|
+
// `ReferenceError: Flushable is not defined` the moment the class is
|
|
113
|
+
// ever loaded. Found for real building Kopular's own event-batching
|
|
114
|
+
// fix — the checker already resolves a cross-module interface's base
|
|
115
|
+
// list entry correctly (it type-checks fine); only codegen's own,
|
|
116
|
+
// separate, same-file-only interfaceNames set didn't know about it.
|
|
117
|
+
importedInterfaceNames = new Set()) {
|
|
118
|
+
this.interfaceNames = new Set([
|
|
119
|
+
...program.statements.filter((s) => s.kind === "InterfaceDecl").map((s) => s.name),
|
|
120
|
+
...importedInterfaceNames,
|
|
121
|
+
]);
|
|
103
122
|
this.outputLine = 1;
|
|
104
123
|
const outFileName = sourceFileName.replace(/\.ks$/, ".js");
|
|
105
124
|
this.sourceMap = new SourceMapBuilder(outFileName, sourceFileName, sourceText);
|
package/dist/diagnostics.js
CHANGED
|
@@ -2,11 +2,11 @@ export class DiagnosticBag {
|
|
|
2
2
|
constructor() {
|
|
3
3
|
this.diagnostics = [];
|
|
4
4
|
}
|
|
5
|
-
error(code, message, line, col) {
|
|
6
|
-
this.diagnostics.push({ code, severity: "error", message, line, col });
|
|
5
|
+
error(code, message, line, col, fix) {
|
|
6
|
+
this.diagnostics.push({ code, severity: "error", message, line, col, ...(fix ? { fix } : {}) });
|
|
7
7
|
}
|
|
8
|
-
warning(code, message, line, col) {
|
|
9
|
-
this.diagnostics.push({ code, severity: "warning", message, line, col });
|
|
8
|
+
warning(code, message, line, col, fix) {
|
|
9
|
+
this.diagnostics.push({ code, severity: "warning", message, line, col, ...(fix ? { fix } : {}) });
|
|
10
10
|
}
|
|
11
11
|
get hasErrors() {
|
|
12
12
|
return this.diagnostics.some((d) => d.severity === "error");
|
|
@@ -17,10 +17,19 @@ export class DiagnosticBag {
|
|
|
17
17
|
.map((d) => {
|
|
18
18
|
const sourceLine = lines[d.line - 1] ?? "";
|
|
19
19
|
const pointer = " ".repeat(Math.max(0, d.col - 1)) + "^";
|
|
20
|
+
const fixLine = d.fix ? `\n fix: ${d.fix}` : "";
|
|
20
21
|
return (`${fileName}:${d.line}:${d.col} - ${d.severity} ${d.code}: ${d.message}\n` +
|
|
21
22
|
` ${sourceLine}\n` +
|
|
22
|
-
` ${pointer}`);
|
|
23
|
+
` ${pointer}${fixLine}`);
|
|
23
24
|
})
|
|
24
25
|
.join("\n\n");
|
|
25
26
|
}
|
|
27
|
+
// Machine-readable form of the exact same diagnostics format() prints —
|
|
28
|
+
// for a caller that wants to act on a compile error programmatically
|
|
29
|
+
// (an editor extension, a CI check, an AI coding agent) instead of
|
|
30
|
+
// scraping formatted text. `fileName` matches format()'s own parameter so
|
|
31
|
+
// both outputs agree on what file each diagnostic is attributed to.
|
|
32
|
+
toJSON(fileName) {
|
|
33
|
+
return { file: fileName, diagnostics: this.diagnostics };
|
|
34
|
+
}
|
|
26
35
|
}
|
package/dist/modules.js
CHANGED
|
@@ -226,6 +226,13 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
226
226
|
for (const absPath of order) {
|
|
227
227
|
const mod = modules.get(absPath);
|
|
228
228
|
const usingExports = new Map();
|
|
229
|
+
// Separate from usingExports (interfaces are excluded there — see its
|
|
230
|
+
// own comment below) — this is what tells codegen's own interfaceNames
|
|
231
|
+
// set about an interface declared in a DIFFERENT file this module
|
|
232
|
+
// `using`s, so a class implementing one doesn't get miscompiled into a
|
|
233
|
+
// real `extends <InterfaceName>` runtime reference (see
|
|
234
|
+
// CodeGenerator.generate's own comment on importedInterfaceNames).
|
|
235
|
+
const importedInterfaceNames = new Set();
|
|
229
236
|
for (const u of mod.program.usings) {
|
|
230
237
|
const depPath = resolve(dirname(absPath), u.path) + ".ks";
|
|
231
238
|
const depExports = exportsByModule.get(depPath);
|
|
@@ -234,13 +241,17 @@ export function compileGraph(entryAbsPath, fileOverrides) {
|
|
|
234
241
|
// they'd make an invalid import specifier if listed here.
|
|
235
242
|
const importableTypeNames = [...depExports.namedTypes.entries()].filter(([, kind]) => kind !== "interface").map(([name]) => name);
|
|
236
243
|
usingExports.set(u.path, [...importableTypeNames, ...depExports.functions.keys(), ...depExports.externValues.keys()]);
|
|
244
|
+
for (const [name, kind] of depExports.namedTypes) {
|
|
245
|
+
if (kind === "interface")
|
|
246
|
+
importedInterfaceNames.add(name);
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
249
|
}
|
|
239
250
|
// Just the basename, not a cwd-relative path — the .js.map file always
|
|
240
251
|
// lands right next to its .ks/.js siblings (ks build writes output in
|
|
241
252
|
// place), so "sources" has to resolve relative to *that* directory, not
|
|
242
253
|
// wherever the compiler happened to be invoked from.
|
|
243
|
-
const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source);
|
|
254
|
+
const { code, map } = new CodeGenerator().generate(mod.program, usingExports, rawContentsByModule.get(absPath) ?? new Map(), basename(absPath), mod.source, importedInterfaceNames);
|
|
244
255
|
outputs.set(absPath, code);
|
|
245
256
|
sourceMaps.set(absPath, map);
|
|
246
257
|
}
|
package/dist/parser.js
CHANGED
|
@@ -528,7 +528,7 @@ export class Parser {
|
|
|
528
528
|
// site"): a type-parameter list appeared somewhere only a function
|
|
529
529
|
// declaration (real or extern) may have one.
|
|
530
530
|
reportStrayTypeParamList(line, col) {
|
|
531
|
-
this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", line, col);
|
|
531
|
+
this.diagnostics.error("KS2023", "A type parameter list is only allowed on a function declaration", line, col, "Remove the type parameter list here, or add '(...)' parameters if this was meant to be a function — a variable (real or extern) can never have its own type parameters, only a function (real or extern) or a class/interface can.");
|
|
532
532
|
}
|
|
533
533
|
// Optional `from "<path>"` (omit for an ambient global) and optional
|
|
534
534
|
// `as "<jsName>"` (omit when the JS-side name matches the KopScript-declared one).
|
package/dist/suggest.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Plain edit-distance "did you mean X?" suggestion — used by the checker
|
|
2
|
+
// wherever a name doesn't resolve (an unknown member, function, identifier,
|
|
3
|
+
// or type name) to point at the closest ACTUALLY-DECLARED name, when one is
|
|
4
|
+
// close enough to plausibly be a typo. Deliberately generic (no knowledge
|
|
5
|
+
// of what kind of name it's comparing — member, function, type, whatever
|
|
6
|
+
// the caller passes as `candidates`), so every "unknown X" diagnostic can
|
|
7
|
+
// reuse the exact same logic instead of each hand-rolling its own.
|
|
8
|
+
function levenshteinDistance(a, b) {
|
|
9
|
+
const rows = a.length + 1;
|
|
10
|
+
const cols = b.length + 1;
|
|
11
|
+
const dp = Array.from({ length: rows }, () => new Array(cols).fill(0));
|
|
12
|
+
for (let i = 0; i < rows; i++)
|
|
13
|
+
dp[i][0] = i;
|
|
14
|
+
for (let j = 0; j < cols; j++)
|
|
15
|
+
dp[0][j] = j;
|
|
16
|
+
for (let i = 1; i < rows; i++) {
|
|
17
|
+
for (let j = 1; j < cols; j++) {
|
|
18
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return dp[a.length][b.length];
|
|
22
|
+
}
|
|
23
|
+
// Returns the closest of `candidates` to `target`, or null if nothing is
|
|
24
|
+
// close enough to be worth suggesting (a totally unrelated name is worse
|
|
25
|
+
// than no suggestion at all — e.g. suggesting 'value' isn't found via
|
|
26
|
+
// "close typo of an existing member" reasoning if the member genuinely
|
|
27
|
+
// doesn't exist anywhere close). The threshold scales with the target's own
|
|
28
|
+
// length: a short name needs a near-exact match, a long one tolerates
|
|
29
|
+
// proportionally more difference.
|
|
30
|
+
export function closestMatch(target, candidates) {
|
|
31
|
+
let best = null;
|
|
32
|
+
let bestDistance = Infinity;
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
if (candidate === target)
|
|
35
|
+
continue; // never suggest the name itself
|
|
36
|
+
const distance = levenshteinDistance(target, candidate);
|
|
37
|
+
if (distance < bestDistance) {
|
|
38
|
+
bestDistance = distance;
|
|
39
|
+
best = candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const threshold = Math.max(2, Math.ceil(target.length / 3));
|
|
43
|
+
return best !== null && bestDistance <= threshold ? best : null;
|
|
44
|
+
}
|
package/package.json
CHANGED