rapydscript-ns 0.9.1 → 0.9.3
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/CHANGELOG.md +22 -1
- package/PYTHON_GAPS.md +420 -0
- package/README.md +154 -30
- package/TODO.md +22 -7
- package/language-service/index.js +241 -12
- package/language-service/language-service.d.ts +1 -1
- package/memory/project_string_impl.md +43 -0
- package/package.json +6 -2
- package/release/baselib-plain-pretty.js +248 -38
- package/release/baselib-plain-ugly.js +8 -8
- package/release/compiler.js +821 -305
- package/release/signatures.json +15 -15
- package/src/ast.pyj +4 -1
- package/src/baselib-builtins.pyj +56 -2
- package/src/baselib-containers.pyj +2 -0
- package/src/baselib-errors.pyj +7 -3
- package/src/baselib-internal.pyj +51 -6
- package/src/baselib-str.pyj +5 -3
- package/src/lib/asyncio.pyj +534 -0
- package/src/lib/base64.pyj +399 -0
- package/src/lib/bisect.pyj +73 -0
- package/src/lib/collections.pyj +1 -1
- package/src/lib/csv.pyj +494 -0
- package/src/lib/heapq.pyj +98 -0
- package/src/lib/html.pyj +382 -0
- package/src/lib/http/__init__.pyj +98 -0
- package/src/lib/http/client.pyj +304 -0
- package/src/lib/http/cookies.pyj +236 -0
- package/src/lib/logging.pyj +672 -0
- package/src/lib/pythonize.pyj +1 -1
- package/src/lib/string.pyj +357 -0
- package/src/lib/textwrap.pyj +329 -0
- package/src/lib/urllib/__init__.pyj +14 -0
- package/src/lib/urllib/error.pyj +66 -0
- package/src/lib/urllib/parse.pyj +475 -0
- package/src/lib/urllib/request.pyj +86 -0
- package/src/monaco-language-service/analyzer.js +5 -2
- package/src/monaco-language-service/completions.js +26 -0
- package/src/monaco-language-service/diagnostics.js +204 -5
- package/src/monaco-language-service/index.js +2 -2
- package/src/monaco-language-service/scope.js +1 -0
- package/src/output/functions.pyj +152 -6
- package/src/output/loops.pyj +26 -2
- package/src/output/modules.pyj +1 -1
- package/src/output/operators.pyj +15 -0
- package/src/output/stream.pyj +0 -1
- package/src/parse.pyj +80 -17
- package/src/tokenizer.pyj +1 -1
- package/test/async_generators.pyj +144 -0
- package/test/asyncio.pyj +307 -0
- package/test/base64.pyj +202 -0
- package/test/bisect.pyj +178 -0
- package/test/csv.pyj +405 -0
- package/test/float_special.pyj +64 -0
- package/test/heapq.pyj +174 -0
- package/test/html.pyj +212 -0
- package/test/http.pyj +259 -0
- package/test/imports.pyj +7 -0
- package/test/logging.pyj +356 -0
- package/test/long.pyj +130 -0
- package/test/parenthesized_with.pyj +141 -0
- package/test/python_compat.pyj +3 -5
- package/test/python_modulo.pyj +76 -0
- package/test/python_modulo_off.pyj +21 -0
- package/test/str.pyj +14 -0
- package/test/string.pyj +245 -0
- package/test/textwrap.pyj +172 -0
- package/test/type_display.pyj +48 -0
- package/test/type_enforcement.pyj +164 -0
- package/test/unit/index.js +80 -6
- package/test/unit/language-service-completions.js +119 -0
- package/test/unit/language-service-scope.js +32 -0
- package/test/unit/language-service.js +128 -4
- package/test/unit/run-language-service.js +17 -3
- package/test/unit/web-repl.js +2094 -29
- package/test/urllib.pyj +193 -0
- package/tools/compile.js +1 -1
- package/tools/compiler.d.ts +367 -0
- package/tools/embedded_compiler.js +7 -7
- package/web-repl/main.js +1 -1
- package/web-repl/rapydscript.js +3 -3
- package/test/omit_function_metadata.pyj +0 -20
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# vim:fileencoding=utf-8
|
|
2
|
+
# Tests for from __python__ import type_enforcement
|
|
3
|
+
# globals: assrt
|
|
4
|
+
|
|
5
|
+
from __python__ import type_enforcement
|
|
6
|
+
|
|
7
|
+
eq = assrt.equal
|
|
8
|
+
de = assrt.deepEqual
|
|
9
|
+
throws = assrt.throws
|
|
10
|
+
ok = assrt.ok
|
|
11
|
+
|
|
12
|
+
def _err(fn):
|
|
13
|
+
"""Call fn and return the TypeError message, or raise if no error."""
|
|
14
|
+
try:
|
|
15
|
+
fn()
|
|
16
|
+
raise AssertionError("Expected TypeError but none was raised")
|
|
17
|
+
except TypeError as e:
|
|
18
|
+
return e.message or str(e)
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# 1. Max positional-arg count
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def _fixed(a, b):
|
|
25
|
+
return a + b
|
|
26
|
+
|
|
27
|
+
throws(def(): _fixed(1, 2, 3);, TypeError)
|
|
28
|
+
eq(_fixed(1, 2), 3)
|
|
29
|
+
|
|
30
|
+
# With defaults
|
|
31
|
+
def _with_def(a, b=10):
|
|
32
|
+
return a + b
|
|
33
|
+
|
|
34
|
+
throws(def(): _with_def(1, 2, 3);, TypeError)
|
|
35
|
+
eq(_with_def(5), 15)
|
|
36
|
+
eq(_with_def(5, 20), 25)
|
|
37
|
+
|
|
38
|
+
# With *args: no max limit
|
|
39
|
+
def _varargs(a, *rest):
|
|
40
|
+
return [a] + list(rest)
|
|
41
|
+
|
|
42
|
+
de(_varargs(1, 2, 3), [1, 2, 3])
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# 2. Missing required positional arg
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
msg = _err(def(): _fixed(1);)
|
|
49
|
+
ok(msg.indexOf('missing required positional argument') >= 0)
|
|
50
|
+
ok(msg.indexOf('b') >= 0)
|
|
51
|
+
|
|
52
|
+
# Simple function (no defaults)
|
|
53
|
+
def _simple(x):
|
|
54
|
+
return x * 2
|
|
55
|
+
|
|
56
|
+
throws(def(): _simple();, TypeError)
|
|
57
|
+
eq(_simple(7), 14)
|
|
58
|
+
|
|
59
|
+
# Required arg can be provided as kwarg (valid for non-posonly)
|
|
60
|
+
eq(_fixed(a=3, b=4), 7)
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# 3. Positional-only enforcement
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def _po(a, b, /):
|
|
67
|
+
return a - b
|
|
68
|
+
|
|
69
|
+
eq(_po(10, 3), 7)
|
|
70
|
+
throws(def(): _po(a=10, b=3);, TypeError)
|
|
71
|
+
throws(def(): _po(b=3);, TypeError)
|
|
72
|
+
|
|
73
|
+
# Mixed: posonly + normal
|
|
74
|
+
def _mixed(x, /, y=5):
|
|
75
|
+
return x + y
|
|
76
|
+
|
|
77
|
+
eq(_mixed(1), 6)
|
|
78
|
+
eq(_mixed(1, y=10), 11)
|
|
79
|
+
throws(def(): _mixed(x=1);, TypeError)
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# 4. Keyword-only enforcement (bare *)
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def _ko(a, *, b):
|
|
86
|
+
return a + b
|
|
87
|
+
|
|
88
|
+
# b is required kwonly; must be passed as keyword
|
|
89
|
+
eq(_ko(1, b=2), 3)
|
|
90
|
+
throws(def(): _ko(1);, TypeError) # b missing
|
|
91
|
+
|
|
92
|
+
def _ko_opt(a, *, b=100):
|
|
93
|
+
return a + b
|
|
94
|
+
|
|
95
|
+
eq(_ko_opt(1), 101)
|
|
96
|
+
eq(_ko_opt(1, b=200), 201)
|
|
97
|
+
|
|
98
|
+
# Cannot pass kwonly positionally (it goes to *args slot which doesn't exist here)
|
|
99
|
+
# Passing positionally puts value into wrong position; enforce checks that b is set
|
|
100
|
+
# In RapydScript, _ko(1, 2) passes 2 positionally but b is kwonly → b === undefined
|
|
101
|
+
throws(def(): _ko(1, 2);, TypeError)
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# 5. Positional-only + keyword-only combined
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _combo(a, b, /, c=1, *, d):
|
|
108
|
+
return a + b + c + d
|
|
109
|
+
|
|
110
|
+
eq(_combo(1, 2, d=10), 14)
|
|
111
|
+
eq(_combo(1, 2, c=3, d=10), 16)
|
|
112
|
+
throws(def(): _combo(a=1, b=2, d=10);, TypeError) # a is posonly
|
|
113
|
+
throws(def(): _combo(1, 2);, TypeError) # d is required kwonly
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# 6. Type annotation enforcement
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def _typed(x: int, y: str):
|
|
120
|
+
return str(x) + y
|
|
121
|
+
|
|
122
|
+
eq(_typed(1, 'a'), '1a')
|
|
123
|
+
throws(def(): _typed('bad', 'a');, TypeError)
|
|
124
|
+
throws(def(): _typed(1, 42);, TypeError)
|
|
125
|
+
|
|
126
|
+
# Type error message mentions the arg name
|
|
127
|
+
msg = _err(def(): _typed('bad', 'ok');)
|
|
128
|
+
ok(msg.indexOf("argument 'x' must be") >= 0)
|
|
129
|
+
|
|
130
|
+
# Annotated with default: type checked when provided
|
|
131
|
+
def _ann_def(n: int = 0):
|
|
132
|
+
return n + 1
|
|
133
|
+
|
|
134
|
+
eq(_ann_def(), 1)
|
|
135
|
+
eq(_ann_def(5), 6)
|
|
136
|
+
throws(def(): _ann_def('five');, TypeError)
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# 7. Class method enforcement
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
class Calc:
|
|
143
|
+
def add(self, a: int, b: int):
|
|
144
|
+
return a + b
|
|
145
|
+
|
|
146
|
+
def greet(self, name, /, *, loud=False):
|
|
147
|
+
return name.upper() if loud else name
|
|
148
|
+
|
|
149
|
+
c = Calc()
|
|
150
|
+
eq(c.add(3, 4), 7)
|
|
151
|
+
throws(def(): c.add(3, 'x');, TypeError) # b must be int
|
|
152
|
+
throws(def(): c.add(3, 4, 5);, TypeError) # too many args
|
|
153
|
+
throws(def(): c.greet(name='Alice');, TypeError) # name is posonly
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# 8. Return annotation stored but not enforced (Python-compatible)
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def _ret(x: int) -> int:
|
|
160
|
+
return x * 2
|
|
161
|
+
|
|
162
|
+
eq(_ret(3), 6)
|
|
163
|
+
# Return type is metadata only — no TypeError from returning wrong type
|
|
164
|
+
# (this matches Python's own behaviour without beartype etc.)
|
package/test/unit/index.js
CHANGED
|
@@ -1831,13 +1831,13 @@ assrt.equal(fib(15), 610)
|
|
|
1831
1831
|
|
|
1832
1832
|
{
|
|
1833
1833
|
name: "print_compiles_to_console_log",
|
|
1834
|
-
description: "print(x) compiles
|
|
1834
|
+
description: "print(x) compiles to console.log(ρσ_str(x)) for Python-style string conversion",
|
|
1835
1835
|
src: [
|
|
1836
1836
|
"# globals: assrt",
|
|
1837
1837
|
"print('hello')",
|
|
1838
1838
|
"print(1, 2, 3)",
|
|
1839
1839
|
].join("\n"),
|
|
1840
|
-
js_checks: ["console.log(\"hello\")", "console.log(1, 2, 3)"],
|
|
1840
|
+
js_checks: ["console.log(ρσ_str(\"hello\"))", "console.log(ρσ_str(1), ρσ_str(2), ρσ_str(3))"],
|
|
1841
1841
|
},
|
|
1842
1842
|
|
|
1843
1843
|
{
|
|
@@ -1883,7 +1883,7 @@ assrt.equal(fib(15), 610)
|
|
|
1883
1883
|
"print('test')",
|
|
1884
1884
|
].join("\n"),
|
|
1885
1885
|
// The compiled JS must NOT contain 'var print = ' (which would overwrite window.print)
|
|
1886
|
-
js_checks: [/console\.log\("test"\)/],
|
|
1886
|
+
js_checks: [/console\.log\(ρσ_str\("test"\)\)/],
|
|
1887
1887
|
},
|
|
1888
1888
|
|
|
1889
1889
|
{
|
|
@@ -5227,6 +5227,72 @@ assrt.equal(fib(15), 610)
|
|
|
5227
5227
|
},
|
|
5228
5228
|
},
|
|
5229
5229
|
|
|
5230
|
+
// ── Strict-mode var declarations ─────────────────────────────────────
|
|
5231
|
+
|
|
5232
|
+
{
|
|
5233
|
+
name: "comprehension_unpack_var_declared",
|
|
5234
|
+
description: "list comprehension with tuple unpacking declares var ρσ_unpack",
|
|
5235
|
+
src: [
|
|
5236
|
+
"# globals: assrt",
|
|
5237
|
+
"pairs = [('a', 1), ('b', 2), ('c', 3)]",
|
|
5238
|
+
"result = [v for k, v in pairs if k != 'b']",
|
|
5239
|
+
"assrt.deepEqual(result, [1, 3])",
|
|
5240
|
+
].join("\n"),
|
|
5241
|
+
js_checks: [/var\s[^;]*ρσ_unpack/],
|
|
5242
|
+
},
|
|
5243
|
+
|
|
5244
|
+
{
|
|
5245
|
+
name: "comprehension_unpack_nested_clauses",
|
|
5246
|
+
description: "nested comprehension with tuple unpacking in inner clause declares ρσ_unpack",
|
|
5247
|
+
src: [
|
|
5248
|
+
"# globals: assrt",
|
|
5249
|
+
"groups = [[(1, 'a'), (2, 'b')], [(3, 'c')]]",
|
|
5250
|
+
"result = [s for row in groups for n, s in row]",
|
|
5251
|
+
"assrt.deepEqual(result, ['a', 'b', 'c'])",
|
|
5252
|
+
].join("\n"),
|
|
5253
|
+
js_checks: [/var\s[^;]*ρσ_unpack/],
|
|
5254
|
+
},
|
|
5255
|
+
|
|
5256
|
+
{
|
|
5257
|
+
name: "with_statement_scope_declared",
|
|
5258
|
+
description: "with statement ρσ_with_exception/ρσ_with_suppress are scope-declared (not implicit globals)",
|
|
5259
|
+
src: [
|
|
5260
|
+
"# globals: assrt",
|
|
5261
|
+
"class CM:",
|
|
5262
|
+
" def __init__(self):",
|
|
5263
|
+
" self.entered = False",
|
|
5264
|
+
" self.exited = False",
|
|
5265
|
+
" def __enter__(self):",
|
|
5266
|
+
" self.entered = True",
|
|
5267
|
+
" return self",
|
|
5268
|
+
" def __exit__(self, exc_type, exc_val, exc_tb):",
|
|
5269
|
+
" self.exited = True",
|
|
5270
|
+
" return False",
|
|
5271
|
+
"with CM() as c:",
|
|
5272
|
+
" assrt.ok(c.entered)",
|
|
5273
|
+
"assrt.ok(c.exited)",
|
|
5274
|
+
].join("\n"),
|
|
5275
|
+
js_checks: [/let\s[^;]*ρσ_with_exception/, /let\s[^;]*ρσ_with_suppress/],
|
|
5276
|
+
},
|
|
5277
|
+
|
|
5278
|
+
{
|
|
5279
|
+
name: "with_statement_suppresses_exception",
|
|
5280
|
+
description: "with statement __exit__ returning True suppresses the exception",
|
|
5281
|
+
src: [
|
|
5282
|
+
"# globals: assrt",
|
|
5283
|
+
"class Suppressor:",
|
|
5284
|
+
" def __enter__(self):",
|
|
5285
|
+
" return self",
|
|
5286
|
+
" def __exit__(self, exc_type, exc_val, exc_tb):",
|
|
5287
|
+
" return True",
|
|
5288
|
+
"caught = False",
|
|
5289
|
+
"with Suppressor():",
|
|
5290
|
+
" raise ValueError('boom')",
|
|
5291
|
+
" caught = True # should not reach",
|
|
5292
|
+
"assrt.ok(not caught, 'line after raise should not execute')",
|
|
5293
|
+
].join("\n"),
|
|
5294
|
+
},
|
|
5295
|
+
|
|
5230
5296
|
];
|
|
5231
5297
|
|
|
5232
5298
|
// ── Runner ───────────────────────────────────────────────────────────────────
|
|
@@ -5310,12 +5376,20 @@ function run_tests(filter) {
|
|
|
5310
5376
|
" – " + test.description);
|
|
5311
5377
|
});
|
|
5312
5378
|
|
|
5379
|
+
var passed = tests.length - failures.length;
|
|
5313
5380
|
console.log("");
|
|
5314
5381
|
if (failures.length) {
|
|
5315
|
-
console.log(colored(
|
|
5316
|
-
|
|
5317
|
-
|
|
5382
|
+
console.log(colored("Failed tests:", "red"));
|
|
5383
|
+
failures.forEach(function (name) {
|
|
5384
|
+
console.log(colored(" ✗ " + name, "red"));
|
|
5385
|
+
});
|
|
5386
|
+
console.log("");
|
|
5318
5387
|
}
|
|
5388
|
+
var summary = "unit tests — " +
|
|
5389
|
+
colored("passed: " + passed, "green") + " " +
|
|
5390
|
+
(failures.length ? colored("failed: " + failures.length, "red") : colored("failed: 0", "green")) +
|
|
5391
|
+
" total: " + tests.length;
|
|
5392
|
+
console.log(summary);
|
|
5319
5393
|
process.exit(failures.length ? 1 : 0);
|
|
5320
5394
|
}
|
|
5321
5395
|
|
|
@@ -87,6 +87,10 @@ function make_tests(CompletionEngine, detect_context, SourceAnalyzer, DtsRegistr
|
|
|
87
87
|
require('path').join(__dirname, '../../src/lib/collections.pyj'), 'utf-8'
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
+
var RE_SRC = require('fs').readFileSync(
|
|
91
|
+
require('path').join(__dirname, '../../src/lib/re.pyj'), 'utf-8'
|
|
92
|
+
);
|
|
93
|
+
|
|
90
94
|
var TESTS = [
|
|
91
95
|
|
|
92
96
|
// ── detect_context ────────────────────────────────────────────────
|
|
@@ -1254,6 +1258,121 @@ function make_tests(CompletionEngine, detect_context, SourceAnalyzer, DtsRegistr
|
|
|
1254
1258
|
},
|
|
1255
1259
|
},
|
|
1256
1260
|
|
|
1261
|
+
// ── Bare import dot completions (import X; X.attr) ──────────────
|
|
1262
|
+
|
|
1263
|
+
{
|
|
1264
|
+
name: "bare_import_virtual_dot_completions",
|
|
1265
|
+
description: "import mymod; mymod. shows module-level symbols from virtual file",
|
|
1266
|
+
run: function () {
|
|
1267
|
+
var vf = {
|
|
1268
|
+
mymod: [
|
|
1269
|
+
"def foo():",
|
|
1270
|
+
" return 1",
|
|
1271
|
+
"def bar():",
|
|
1272
|
+
" return 2",
|
|
1273
|
+
"BAZ = 42",
|
|
1274
|
+
].join("\n"),
|
|
1275
|
+
};
|
|
1276
|
+
var engine = make_engine(vf);
|
|
1277
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1278
|
+
var scopeMap = analyzer.analyze("import mymod\npass", { virtualFiles: vf });
|
|
1279
|
+
var list = engine.getCompletions(scopeMap, pos(2, 7), "mymod.", MockKind);
|
|
1280
|
+
assert_has(list, 'foo', 'foo from virtual module');
|
|
1281
|
+
assert_has(list, 'bar', 'bar from virtual module');
|
|
1282
|
+
assert_has(list, 'BAZ', 'BAZ from virtual module');
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
|
|
1286
|
+
{
|
|
1287
|
+
name: "bare_import_virtual_dot_prefix",
|
|
1288
|
+
description: "import mymod; mymod.f filters completions by prefix",
|
|
1289
|
+
run: function () {
|
|
1290
|
+
var vf = {
|
|
1291
|
+
mymod: "def foo():\n return 1\ndef bar():\n return 2",
|
|
1292
|
+
};
|
|
1293
|
+
var engine = make_engine(vf);
|
|
1294
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1295
|
+
var scopeMap = analyzer.analyze("import mymod\npass", { virtualFiles: vf });
|
|
1296
|
+
var list = engine.getCompletions(scopeMap, pos(2, 8), "mymod.f", MockKind);
|
|
1297
|
+
assert_has(list, 'foo', 'foo matches prefix f');
|
|
1298
|
+
assert_missing(list,'bar', 'bar does not match prefix f');
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
|
|
1302
|
+
{
|
|
1303
|
+
name: "bare_import_re_dot_completions",
|
|
1304
|
+
description: "import re; re. shows re module exports via stdlibFiles",
|
|
1305
|
+
run: function () {
|
|
1306
|
+
var engine = make_engine({}, ['print', 'len', 'range'], null, null, { re: RE_SRC });
|
|
1307
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1308
|
+
var scopeMap = analyzer.analyze("import re\npass", { virtualFiles: { re: RE_SRC } });
|
|
1309
|
+
var list = engine.getCompletions(scopeMap, pos(2, 4), "re.", MockKind);
|
|
1310
|
+
assert_has(list, 'match', 'match from re module');
|
|
1311
|
+
assert_has(list, 'search', 'search from re module');
|
|
1312
|
+
assert_has(list, 'compile', 'compile from re module');
|
|
1313
|
+
assert_has(list, 'sub', 'sub from re module');
|
|
1314
|
+
assert_has(list, 'findall', 'findall from re module');
|
|
1315
|
+
assert_has(list, 'split', 'split from re module');
|
|
1316
|
+
},
|
|
1317
|
+
},
|
|
1318
|
+
|
|
1319
|
+
{
|
|
1320
|
+
name: "bare_import_re_dot_prefix",
|
|
1321
|
+
description: "import re; re.ma filters to matching names like match",
|
|
1322
|
+
run: function () {
|
|
1323
|
+
var engine = make_engine({}, ['print', 'len', 'range'], null, null, { re: RE_SRC });
|
|
1324
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1325
|
+
var scopeMap = analyzer.analyze("import re\npass", { virtualFiles: { re: RE_SRC } });
|
|
1326
|
+
var list = engine.getCompletions(scopeMap, pos(2, 6), "re.ma", MockKind);
|
|
1327
|
+
assert_has(list, 'match', 'match matches prefix ma');
|
|
1328
|
+
assert_missing(list,'compile', 'compile does not match prefix ma');
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1332
|
+
{
|
|
1333
|
+
name: "bare_import_alias_dot_completions",
|
|
1334
|
+
description: "import mymod as mm; mm. shows module-level symbols",
|
|
1335
|
+
run: function () {
|
|
1336
|
+
var vf = {
|
|
1337
|
+
mymod: "def foo():\n return 1\nVAL = 10",
|
|
1338
|
+
};
|
|
1339
|
+
var engine = make_engine(vf);
|
|
1340
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1341
|
+
var scopeMap = analyzer.analyze("import mymod as mm\npass", { virtualFiles: vf });
|
|
1342
|
+
var list = engine.getCompletions(scopeMap, pos(2, 4), "mm.", MockKind);
|
|
1343
|
+
assert_has(list, 'foo', 'foo from aliased module');
|
|
1344
|
+
assert_has(list, 'VAL', 'VAL from aliased module');
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
|
|
1348
|
+
{
|
|
1349
|
+
name: "bare_import_unknown_module_empty",
|
|
1350
|
+
description: "import unknown; unknown. returns empty when module source unavailable",
|
|
1351
|
+
run: function () {
|
|
1352
|
+
var engine = make_engine({});
|
|
1353
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1354
|
+
var scopeMap = analyzer.analyze("import unknown\npass", {});
|
|
1355
|
+
var list = engine.getCompletions(scopeMap, pos(2, 9), "unknown.", MockKind);
|
|
1356
|
+
assert.strictEqual(list.suggestions.length, 0, 'unknown module → no dot suggestions');
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
|
|
1360
|
+
{
|
|
1361
|
+
name: "bare_import_collections_dot_completions",
|
|
1362
|
+
description: "import collections; collections. shows namedtuple, deque, Counter, etc.",
|
|
1363
|
+
run: function () {
|
|
1364
|
+
var engine = make_engine({}, ['print', 'len', 'range'], null, null, { collections: COLLECTIONS_SRC });
|
|
1365
|
+
var analyzer = new SourceAnalyzer(RS);
|
|
1366
|
+
var scopeMap = analyzer.analyze("import collections\npass", { virtualFiles: { collections: COLLECTIONS_SRC } });
|
|
1367
|
+
var list = engine.getCompletions(scopeMap, pos(2, 13), "collections.", MockKind);
|
|
1368
|
+
assert_has(list, 'namedtuple', 'namedtuple from collections');
|
|
1369
|
+
assert_has(list, 'deque', 'deque from collections');
|
|
1370
|
+
assert_has(list, 'Counter', 'Counter from collections');
|
|
1371
|
+
assert_has(list, 'OrderedDict', 'OrderedDict from collections');
|
|
1372
|
+
assert_has(list, 'defaultdict', 'defaultdict from collections');
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
|
|
1257
1376
|
{
|
|
1258
1377
|
name: "return_type_imported_fn_inferred_consistent_branches",
|
|
1259
1378
|
description: "imported fn with multiple return [] branches — type still inferred as list",
|
|
@@ -613,6 +613,38 @@ function make_tests(SourceAnalyzer, RS) {
|
|
|
613
613
|
},
|
|
614
614
|
},
|
|
615
615
|
|
|
616
|
+
{
|
|
617
|
+
name: "async_generator_local_var_visible",
|
|
618
|
+
description: "Inside `async def` with `yield`, local vars are tracked in scope",
|
|
619
|
+
run: function () {
|
|
620
|
+
// Async generators (async def + yield) should not break scope analysis.
|
|
621
|
+
// Local assignments should be picked up the same as in regular functions.
|
|
622
|
+
var m = analyze([
|
|
623
|
+
"async def aiter():",
|
|
624
|
+
" counter = 0",
|
|
625
|
+
" yield counter",
|
|
626
|
+
" counter += 1",
|
|
627
|
+
" yield counter",
|
|
628
|
+
].join("\n"));
|
|
629
|
+
var sym = find(m.getAllSymbols(), "counter");
|
|
630
|
+
assert.ok(sym, "Expected 'counter' symbol in async-generator scope");
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
{
|
|
635
|
+
name: "async_for_loop_var_visible",
|
|
636
|
+
description: "`async for x in ...` loop variable is registered in scope",
|
|
637
|
+
run: function () {
|
|
638
|
+
var m = analyze([
|
|
639
|
+
"async def consume(it):",
|
|
640
|
+
" async for item in it:",
|
|
641
|
+
" print(item)",
|
|
642
|
+
].join("\n"));
|
|
643
|
+
var sym = find(m.getAllSymbols(), "item");
|
|
644
|
+
assert.ok(sym, "Expected 'item' loop variable in async-for scope");
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
|
|
616
648
|
{
|
|
617
649
|
name: "inferred_class_no_inference_for_variable_rhs",
|
|
618
650
|
description: "x = some_var does not set inferred_class (unknown rhs)",
|
|
@@ -61,12 +61,27 @@ var LIB_DIR = path.join(__dirname, "../../src/lib");
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Return the module names of all .pyj files currently in src/lib/.
|
|
64
|
+
* Also includes sub-package directory names that contain an __init__.pyj.
|
|
64
65
|
* Excludes cache files (.pyj-cached) and any non-.pyj entries.
|
|
65
66
|
*/
|
|
66
67
|
function get_lib_module_names() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
var names = [];
|
|
69
|
+
fs.readdirSync(LIB_DIR).forEach(function (f) {
|
|
70
|
+
if (f.endsWith(".pyj-cached")) return;
|
|
71
|
+
if (f.endsWith(".pyj")) {
|
|
72
|
+
names.push(f.replace(/\.pyj$/, ""));
|
|
73
|
+
} else {
|
|
74
|
+
// Directory with __init__.pyj counts as a package module.
|
|
75
|
+
var full = path.join(LIB_DIR, f);
|
|
76
|
+
try {
|
|
77
|
+
var stat = fs.statSync(full);
|
|
78
|
+
if (stat.isDirectory() && fs.existsSync(path.join(full, "__init__.pyj"))) {
|
|
79
|
+
names.push(f);
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return names;
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
function make_tests(Diagnostics, RS, STDLIB_MODULES) {
|
|
@@ -173,7 +188,7 @@ function make_tests(Diagnostics, RS, STDLIB_MODULES) {
|
|
|
173
188
|
name: "undef_suppressed_extra_builtins",
|
|
174
189
|
description: "extraBuiltins option prevents a name from being flagged",
|
|
175
190
|
run: function () {
|
|
176
|
-
var inst = new Diagnostics(RS,
|
|
191
|
+
var inst = new Diagnostics(RS, ['MY_GLOBAL', 'ANOTHER']);
|
|
177
192
|
var markers = inst.check("print(MY_GLOBAL)\nprint(ANOTHER)");
|
|
178
193
|
assert.deepStrictEqual(markers, []);
|
|
179
194
|
},
|
|
@@ -1467,6 +1482,115 @@ function make_tests(Diagnostics, RS, STDLIB_MODULES) {
|
|
|
1467
1482
|
},
|
|
1468
1483
|
},
|
|
1469
1484
|
|
|
1485
|
+
// ── Infinite-loop detection ───────────────────────────────────────
|
|
1486
|
+
|
|
1487
|
+
{
|
|
1488
|
+
name: "infinite_loop_while_true",
|
|
1489
|
+
description: "while True: with no await inside is flagged as a warning",
|
|
1490
|
+
run: function () {
|
|
1491
|
+
var markers = d().check([
|
|
1492
|
+
"while True:",
|
|
1493
|
+
" x = 1",
|
|
1494
|
+
].join("\n"));
|
|
1495
|
+
var warnings = markers.filter(function (m) {
|
|
1496
|
+
return m.severity === SEV_WARNING &&
|
|
1497
|
+
m.message.indexOf("while loop") !== -1;
|
|
1498
|
+
});
|
|
1499
|
+
assert.ok(warnings.length >= 1,
|
|
1500
|
+
"Expected an infinite-loop warning but got: " + JSON.stringify(markers));
|
|
1501
|
+
},
|
|
1502
|
+
},
|
|
1503
|
+
|
|
1504
|
+
{
|
|
1505
|
+
name: "infinite_loop_while_1",
|
|
1506
|
+
description: "while 1: with no await inside is flagged as a warning",
|
|
1507
|
+
run: function () {
|
|
1508
|
+
var markers = d().check([
|
|
1509
|
+
"while 1:",
|
|
1510
|
+
" pass",
|
|
1511
|
+
].join("\n"));
|
|
1512
|
+
var warnings = markers.filter(function (m) {
|
|
1513
|
+
return m.severity === SEV_WARNING &&
|
|
1514
|
+
m.message.indexOf("while loop") !== -1;
|
|
1515
|
+
});
|
|
1516
|
+
assert.ok(warnings.length >= 1,
|
|
1517
|
+
"Expected an infinite-loop warning for while 1: but got: " + JSON.stringify(markers));
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
|
|
1521
|
+
{
|
|
1522
|
+
name: "infinite_loop_suppressed_by_await",
|
|
1523
|
+
description: "while True: containing an await does NOT produce a warning",
|
|
1524
|
+
run: function () {
|
|
1525
|
+
var markers = d().check([
|
|
1526
|
+
"async def run():",
|
|
1527
|
+
" while True:",
|
|
1528
|
+
" await some_task()",
|
|
1529
|
+
].join("\n"));
|
|
1530
|
+
var inf = markers.filter(function (m) {
|
|
1531
|
+
return m.message.indexOf("while loop") !== -1;
|
|
1532
|
+
});
|
|
1533
|
+
assert.deepStrictEqual(inf, [],
|
|
1534
|
+
"Expected no infinite-loop warning when await is present but got: " + JSON.stringify(inf));
|
|
1535
|
+
},
|
|
1536
|
+
},
|
|
1537
|
+
|
|
1538
|
+
{
|
|
1539
|
+
name: "async_generator_no_diagnostics",
|
|
1540
|
+
description: "async def with yield (async generator) parses cleanly with no syntax errors",
|
|
1541
|
+
run: function () {
|
|
1542
|
+
var markers = d().check([
|
|
1543
|
+
"async def aiter():",
|
|
1544
|
+
" yield 1",
|
|
1545
|
+
" yield 2",
|
|
1546
|
+
"",
|
|
1547
|
+
"async def consume():",
|
|
1548
|
+
" out = []",
|
|
1549
|
+
" async for x in aiter():",
|
|
1550
|
+
" out.append(x)",
|
|
1551
|
+
" return out",
|
|
1552
|
+
].join("\n"));
|
|
1553
|
+
var errs = markers.filter(function (m) { return m.severity === 1 || m.severity === 8; });
|
|
1554
|
+
assert.deepStrictEqual(errs, [],
|
|
1555
|
+
"Expected no syntax errors for async generator + async for, got: " + JSON.stringify(errs));
|
|
1556
|
+
},
|
|
1557
|
+
},
|
|
1558
|
+
|
|
1559
|
+
{
|
|
1560
|
+
name: "async_generator_local_no_undef",
|
|
1561
|
+
description: "Local vars inside an async generator (with `await` and `yield`) resolve correctly",
|
|
1562
|
+
run: function () {
|
|
1563
|
+
var markers = d().check([
|
|
1564
|
+
"async def stream():",
|
|
1565
|
+
" n = await Promise.resolve(3)",
|
|
1566
|
+
" i = 0",
|
|
1567
|
+
" while i < n:",
|
|
1568
|
+
" yield i",
|
|
1569
|
+
" i += 1",
|
|
1570
|
+
].join("\n"));
|
|
1571
|
+
var undef = markers.filter(function (m) { return m.message.indexOf("Undefined symbol") !== -1; });
|
|
1572
|
+
assert.deepStrictEqual(undef, [],
|
|
1573
|
+
"Expected no 'Undefined symbol' errors in async generator, got: " + JSON.stringify(undef));
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
|
|
1577
|
+
{
|
|
1578
|
+
name: "infinite_loop_no_false_positive_for_condition",
|
|
1579
|
+
description: "while x > 0: (non-constant condition) does not produce a warning",
|
|
1580
|
+
run: function () {
|
|
1581
|
+
var markers = d().check([
|
|
1582
|
+
"x = 10",
|
|
1583
|
+
"while x > 0:",
|
|
1584
|
+
" x -= 1",
|
|
1585
|
+
].join("\n"));
|
|
1586
|
+
var inf = markers.filter(function (m) {
|
|
1587
|
+
return m.message.indexOf("while loop") !== -1;
|
|
1588
|
+
});
|
|
1589
|
+
assert.deepStrictEqual(inf, [],
|
|
1590
|
+
"Expected no infinite-loop warning for non-constant condition but got: " + JSON.stringify(inf));
|
|
1591
|
+
},
|
|
1592
|
+
},
|
|
1593
|
+
|
|
1470
1594
|
];
|
|
1471
1595
|
|
|
1472
1596
|
return TESTS;
|
|
@@ -29,7 +29,7 @@ var FILES = [
|
|
|
29
29
|
"web-repl.js",
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
-
var
|
|
32
|
+
var failed_files = [];
|
|
33
33
|
|
|
34
34
|
FILES.forEach(function (file) {
|
|
35
35
|
var result = spawn(
|
|
@@ -37,7 +37,21 @@ FILES.forEach(function (file) {
|
|
|
37
37
|
[path.join(__dirname, file)],
|
|
38
38
|
{ stdio: "inherit" }
|
|
39
39
|
);
|
|
40
|
-
if (result.status !== 0)
|
|
40
|
+
if (result.status !== 0) failed_files.push(file);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
console.log("");
|
|
44
|
+
if (failed_files.length) {
|
|
45
|
+
console.log(colored("Failed test files:", "red"));
|
|
46
|
+
failed_files.forEach(function (file) {
|
|
47
|
+
console.log(colored(" ✗ " + file, "red"));
|
|
48
|
+
});
|
|
49
|
+
console.log("");
|
|
50
|
+
}
|
|
51
|
+
var passed_count = FILES.length - failed_files.length;
|
|
52
|
+
var summary = "test suites — " +
|
|
53
|
+
colored("passed: " + passed_count, "green") + " " +
|
|
54
|
+
(failed_files.length ? colored("failed: " + failed_files.length, "red") : colored("failed: 0", "green")) +
|
|
55
|
+
" total: " + FILES.length;
|
|
56
|
+
console.log(summary);
|
|
57
|
+
process.exit(failed_files.length ? 1 : 0);
|