rapydscript-ns 0.8.2 → 0.8.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/PYTHON_DIFFERENCES_REPORT.md +291 -0
  3. package/PYTHON_FEATURE_COVERAGE.md +96 -5
  4. package/README.md +161 -46
  5. package/TODO.md +2 -283
  6. package/language-service/index.js +4474 -0
  7. package/language-service/language-service.d.ts +40 -0
  8. package/package.json +9 -7
  9. package/src/baselib-builtins.pyj +77 -1
  10. package/src/baselib-containers.pyj +8 -4
  11. package/src/baselib-internal.pyj +30 -1
  12. package/src/baselib-str.pyj +8 -1
  13. package/src/lib/collections.pyj +1 -1
  14. package/src/lib/numpy.pyj +10 -10
  15. package/src/monaco-language-service/analyzer.js +131 -9
  16. package/src/monaco-language-service/builtins.js +12 -2
  17. package/src/monaco-language-service/completions.js +170 -1
  18. package/src/monaco-language-service/diagnostics.js +1 -1
  19. package/src/monaco-language-service/index.js +17 -0
  20. package/src/monaco-language-service/scope.js +3 -0
  21. package/src/output/classes.pyj +20 -3
  22. package/src/output/codegen.pyj +1 -1
  23. package/src/output/functions.pyj +4 -16
  24. package/src/output/loops.pyj +0 -9
  25. package/src/output/modules.pyj +1 -4
  26. package/src/output/operators.pyj +14 -0
  27. package/src/output/stream.pyj +0 -13
  28. package/src/parse.pyj +17 -1
  29. package/test/baselib.pyj +4 -4
  30. package/test/classes.pyj +56 -17
  31. package/test/collections.pyj +5 -5
  32. package/test/python_compat.pyj +326 -0
  33. package/test/python_features.pyj +110 -23
  34. package/test/slice.pyj +105 -0
  35. package/test/str.pyj +25 -0
  36. package/test/unit/fixtures/fibonacci_expected.js +1 -1
  37. package/test/unit/index.js +119 -7
  38. package/test/unit/language-service-builtins.js +70 -0
  39. package/test/unit/language-service-bundle.js +5 -5
  40. package/test/unit/language-service-completions.js +180 -0
  41. package/test/unit/language-service-index.js +350 -0
  42. package/test/unit/language-service-scope.js +255 -0
  43. package/test/unit/language-service.js +35 -0
  44. package/test/unit/run-language-service.js +1 -0
  45. package/test/unit/web-repl.js +134 -0
  46. package/tools/build-language-service.js +2 -2
  47. package/tools/compiler.js +0 -24
  48. package/tools/export.js +3 -37
  49. package/web-repl/rapydscript.js +6 -40
  50. package/web-repl/language-service.js +0 -4187
@@ -0,0 +1,350 @@
1
+ /*
2
+ * test/unit/language-service-index.js
3
+ *
4
+ * Tests for src/monaco-language-service/index.js (RapydScriptLanguageService).
5
+ * Focuses on the automatic virtualFiles population from open Monaco models,
6
+ * which enables cross-file return-type inference without explicit setVirtualFiles calls.
7
+ *
8
+ * Usage:
9
+ * node test/unit/language-service-index.js # run all tests
10
+ * node test/unit/language-service-index.js <test-name> # run a single test
11
+ */
12
+ "use strict";
13
+
14
+ var assert = require("assert");
15
+ var path = require("path");
16
+ var url = require("url");
17
+ var fs = require("fs");
18
+ var compiler_module = require("../../tools/compiler");
19
+ var utils = require("../../tools/utils");
20
+ var colored = utils.safe_colored;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Mock Monaco helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Create a minimal mock Monaco environment.
28
+ * initial_models: models that exist before registerRapydScript is called.
29
+ */
30
+ function make_mock_monaco(initial_models) {
31
+ var models = (initial_models || []).slice();
32
+ var create_listeners = [];
33
+ var dispose_listeners = [];
34
+
35
+ var mock = {
36
+ MarkerSeverity: { Error: 8, Warning: 4, Info: 2, Hint: 1 },
37
+ languages: {
38
+ getLanguages: function() { return []; },
39
+ register: function() {},
40
+ registerCompletionItemProvider: function() { return { dispose: function() {} }; },
41
+ registerSignatureHelpProvider: function() { return { dispose: function() {} }; },
42
+ registerHoverProvider: function() { return { dispose: function() {} }; },
43
+ setMonarchTokensProvider: function() {},
44
+ setLanguageConfiguration: function() {},
45
+ CompletionItemKind: { Function: 1, Variable: 6, Class: 5, Module: 8, Keyword: 17, Method: 0 },
46
+ },
47
+ editor: {
48
+ getModels: function() { return models.slice(); },
49
+ setModelMarkers: function() {},
50
+ onDidCreateModel: function(cb) { create_listeners.push(cb); return { dispose: function() {} }; },
51
+ onWillDisposeModel: function(cb) { dispose_listeners.push(cb); return { dispose: function() {} }; },
52
+ },
53
+ // Test helpers — simulate adding/removing a model after service creation
54
+ _addModel: function(model) {
55
+ models.push(model);
56
+ create_listeners.forEach(function(cb) { cb(model); });
57
+ },
58
+ _disposeModel: function(model) {
59
+ models = models.filter(function(m) { return m !== model; });
60
+ dispose_listeners.forEach(function(cb) { cb(model); });
61
+ },
62
+ };
63
+ return mock;
64
+ }
65
+
66
+ /**
67
+ * Create a minimal mock Monaco TextModel.
68
+ * @param {string} code - source content
69
+ * @param {string} uri_path - the path portion of the URI (e.g. '/scripts/utils.py')
70
+ * @param {string} [lang] - language id (default: 'rapydscript')
71
+ */
72
+ function make_model(code, uri_path, lang) {
73
+ lang = lang || 'rapydscript';
74
+ var listeners = [];
75
+ var current_code = code;
76
+ return {
77
+ getLanguageId: function() { return lang; },
78
+ getValue: function() { return current_code; },
79
+ uri: { path: uri_path || '/unnamed.py' },
80
+ onDidChangeContent: function(cb) { listeners.push(cb); return { dispose: function() {} }; },
81
+ // Test helper — simulate an edit
82
+ _setContent: function(new_code) {
83
+ current_code = new_code;
84
+ listeners.forEach(function(cb) { cb({ changes: [] }); });
85
+ },
86
+ };
87
+ }
88
+
89
+ /** Wait for all pending setTimeout(fn, 0) callbacks to fire. */
90
+ function flush(ms) {
91
+ return new Promise(function(resolve) { setTimeout(resolve, ms || 20); });
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Test definitions (each test returns a Promise or is sync)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function make_tests(registerRapydScript, RS) {
99
+
100
+ var builtins_src = fs.readFileSync(
101
+ path.join(__dirname, '../../dev/baselib-plain-pretty.js'), 'utf-8'
102
+ );
103
+
104
+ function make_service(models, extra_opts) {
105
+ var compiler = RS.web_repl ? RS.web_repl() : RS;
106
+ var monaco = make_mock_monaco(models);
107
+ var svc = registerRapydScript(monaco, Object.assign({
108
+ compiler: compiler,
109
+ }, extra_opts || {}));
110
+ return { svc: svc, monaco: monaco };
111
+ }
112
+
113
+ var TESTS = [
114
+
115
+ // ── Module name derivation ────────────────────────────────────────
116
+
117
+ {
118
+ name: "module_name_from_uri_path",
119
+ description: "ModelState derives the module name from the URI path (strip dir + .py extension)",
120
+ run_async: function() {
121
+ var utils_model = make_model("def foo():\n return []\n", "/scripts/utils.py");
122
+ var env = make_service([utils_model]);
123
+ return flush().then(function() {
124
+ assert.ok(
125
+ Object.prototype.hasOwnProperty.call(env.svc._virtualFiles, 'utils'),
126
+ "Expected 'utils' in _virtualFiles after model _run(); got keys: " +
127
+ JSON.stringify(Object.keys(env.svc._virtualFiles))
128
+ );
129
+ assert.strictEqual(
130
+ env.svc._virtualFiles['utils'],
131
+ "def foo():\n return []\n",
132
+ "virtualFiles['utils'] should contain the model source"
133
+ );
134
+ });
135
+ },
136
+ },
137
+
138
+ {
139
+ name: "module_name_pyj_extension",
140
+ description: "ModelState also handles .pyj extensions",
141
+ run_async: function() {
142
+ var model = make_model("x = 1\n", "/lib/helpers.pyj");
143
+ var env = make_service([model]);
144
+ return flush().then(function() {
145
+ assert.ok(
146
+ Object.prototype.hasOwnProperty.call(env.svc._virtualFiles, 'helpers'),
147
+ "Expected 'helpers' in _virtualFiles for .pyj model"
148
+ );
149
+ });
150
+ },
151
+ },
152
+
153
+ // ── Auto-population on model attach ───────────────────────────────
154
+
155
+ {
156
+ name: "virtual_files_populated_on_attach",
157
+ description: "Opening a second model after service creation populates _virtualFiles",
158
+ run_async: function() {
159
+ var main_model = make_model("from utils import get_items\n", "/main.py");
160
+ var utils_model = make_model("def get_items():\n return []\n", "/utils.py");
161
+ var env = make_service([main_model]);
162
+ // utils.py is opened after initial service creation
163
+ env.monaco._addModel(utils_model);
164
+ return flush().then(function() {
165
+ assert.ok(
166
+ Object.prototype.hasOwnProperty.call(env.svc._virtualFiles, 'utils'),
167
+ "Expected 'utils' in _virtualFiles after model added dynamically"
168
+ );
169
+ });
170
+ },
171
+ },
172
+
173
+ // ── Cleanup on model detach ───────────────────────────────────────
174
+
175
+ {
176
+ name: "virtual_files_cleaned_on_detach",
177
+ description: "Closing a model removes its entry from _virtualFiles",
178
+ run_async: function() {
179
+ var utils_model = make_model("def get_items():\n return []\n", "/utils.py");
180
+ var env = make_service([utils_model]);
181
+ return flush().then(function() {
182
+ assert.ok(
183
+ Object.prototype.hasOwnProperty.call(env.svc._virtualFiles, 'utils'),
184
+ "Pre-condition: 'utils' should be in _virtualFiles before detach"
185
+ );
186
+ env.monaco._disposeModel(utils_model);
187
+ assert.ok(
188
+ !Object.prototype.hasOwnProperty.call(env.svc._virtualFiles, 'utils'),
189
+ "Expected 'utils' to be removed from _virtualFiles after model detach"
190
+ );
191
+ });
192
+ },
193
+ },
194
+
195
+ // ── Content updates ───────────────────────────────────────────────
196
+
197
+ {
198
+ name: "virtual_files_updated_on_content_change",
199
+ description: "Editing a model updates its entry in _virtualFiles",
200
+ run_async: function() {
201
+ var model = make_model("def get_items():\n return []\n", "/utils.py");
202
+ // Use parseDelay:0 so the re-run fires immediately after content change.
203
+ var env = make_service([model], { parseDelay: 0 });
204
+ return flush().then(function() {
205
+ model._setContent("def get_items():\n return {}\n");
206
+ return flush(20);
207
+ }).then(function() {
208
+ assert.strictEqual(
209
+ env.svc._virtualFiles['utils'],
210
+ "def get_items():\n return {}\n",
211
+ "_virtualFiles['utils'] should reflect the updated content"
212
+ );
213
+ });
214
+ },
215
+ },
216
+
217
+ // ── Cross-file inferred return type via auto-populated virtualFiles ─
218
+
219
+ {
220
+ name: "cross_file_inferred_return_type_list",
221
+ description: "Importing from an open model resolves inferred return_type='list' for completions",
222
+ run_async: function() {
223
+ var builtins_mod = require(path.join(__dirname, '../../src/monaco-language-service/builtins.js'));
224
+ var SourceAnalyzer = require(url.fileURLToPath(url.pathToFileURL(
225
+ path.join(__dirname, '../../src/monaco-language-service/analyzer.js')
226
+ ).href));
227
+
228
+ var utils_model = make_model(
229
+ "def get_items():\n return []\n",
230
+ "/utils.py"
231
+ );
232
+ var env = make_service([utils_model]);
233
+
234
+ return flush().then(function() {
235
+ // _virtualFiles['utils'] should now be populated automatically
236
+ assert.ok(
237
+ env.svc._virtualFiles['utils'],
238
+ "Pre-condition: virtualFiles['utils'] must be set"
239
+ );
240
+
241
+ // Simulate the current file importing from utils
242
+ var CompletionEngine = require(url.fileURLToPath(url.pathToFileURL(
243
+ path.join(__dirname, '../../src/monaco-language-service/completions.js')
244
+ ).href));
245
+ var analyzer = new SourceAnalyzer.SourceAnalyzer(RS);
246
+ var engine = new CompletionEngine.CompletionEngine(analyzer, {
247
+ virtualFiles: env.svc._virtualFiles,
248
+ builtinNames: ['print', 'len'],
249
+ builtinsRegistry: new builtins_mod.BuiltinsRegistry(),
250
+ });
251
+
252
+ var scopeMap = analyzer.analyze([
253
+ "from utils import get_items",
254
+ "items = get_items()",
255
+ "pass",
256
+ ].join("\n"), { virtualFiles: env.svc._virtualFiles });
257
+
258
+ // items. should suggest list members
259
+ var MockKind = { Function: 1, Variable: 6 };
260
+ var list = engine.getCompletions(scopeMap, { lineNumber: 3, column: 8 }, "items.", MockKind);
261
+ var labels = list.suggestions.map(function(s) { return s.label; });
262
+ assert.ok(labels.indexOf('append') !== -1,
263
+ "Expected 'append' in completions for items. (inferred list); got: " + JSON.stringify(labels));
264
+ });
265
+ },
266
+ },
267
+
268
+ ];
269
+
270
+ return TESTS;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Async-aware runner
275
+ // ---------------------------------------------------------------------------
276
+
277
+ function run_tests(TESTS, filter) {
278
+ var tests = filter
279
+ ? TESTS.filter(function(t) { return t.name === filter; })
280
+ : TESTS;
281
+
282
+ if (tests.length === 0) {
283
+ console.error(colored("No test found: " + filter, "red"));
284
+ process.exit(1);
285
+ }
286
+
287
+ var failures = [];
288
+
289
+ function run_next(i) {
290
+ if (i >= tests.length) {
291
+ console.log("");
292
+ if (failures.length) {
293
+ console.log(colored(failures.length + " test(s) failed.", "red"));
294
+ process.exit(1);
295
+ } else {
296
+ console.log(colored("All " + tests.length + " language-service-index tests passed!", "green"));
297
+ process.exit(0);
298
+ }
299
+ return;
300
+ }
301
+
302
+ var test = tests[i];
303
+
304
+ function on_pass() {
305
+ console.log(colored("PASS " + test.name, "green") + " \u2013 " + test.description);
306
+ run_next(i + 1);
307
+ }
308
+ function on_fail(e) {
309
+ failures.push(test.name);
310
+ console.log(colored("FAIL " + test.name, "red") +
311
+ "\n " + (e.message || String(e)) + "\n");
312
+ run_next(i + 1);
313
+ }
314
+
315
+ if (typeof test.run_async === "function") {
316
+ Promise.resolve().then(function() {
317
+ return test.run_async();
318
+ }).then(on_pass).catch(on_fail);
319
+ } else {
320
+ try {
321
+ test.run();
322
+ on_pass();
323
+ } catch (e) {
324
+ on_fail(e);
325
+ }
326
+ }
327
+ }
328
+
329
+ run_next(0);
330
+ }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Entry point
334
+ // ---------------------------------------------------------------------------
335
+
336
+ var index_path = url.pathToFileURL(
337
+ path.join(__dirname, "../../src/monaco-language-service/index.js")
338
+ ).href;
339
+
340
+ var filter = process.argv[2] || null;
341
+
342
+ import(index_path).then(function(mod) {
343
+ var registerRapydScript = mod.registerRapydScript;
344
+ var RS = compiler_module.create_compiler();
345
+ var TESTS = make_tests(registerRapydScript, RS);
346
+ run_tests(TESTS, filter);
347
+ }).catch(function(e) {
348
+ console.error(colored("Failed to load index module:", "red"), e);
349
+ process.exit(1);
350
+ });
@@ -770,6 +770,261 @@ function make_tests(SourceAnalyzer, RS) {
770
770
  },
771
771
  },
772
772
 
773
+ // ── Return type inference ─────────────────────────────────────────
774
+
775
+ {
776
+ name: "infer_return_type_list_literal",
777
+ description: "return [] infers return_type = 'list'",
778
+ run: function () {
779
+ var m = analyze("def get_items():\n return []");
780
+ var sym = find(m.getAllSymbols(), "get_items");
781
+ assert.ok(sym, "Expected 'get_items' symbol");
782
+ assert.strictEqual(sym.return_type, "list",
783
+ "Expected return_type 'list', got: " + sym.return_type);
784
+ },
785
+ },
786
+
787
+ {
788
+ name: "infer_return_type_dict_literal",
789
+ description: "return {} infers return_type = 'dict'",
790
+ run: function () {
791
+ var m = analyze("def get_map():\n return {}");
792
+ var sym = find(m.getAllSymbols(), "get_map");
793
+ assert.ok(sym, "Expected 'get_map' symbol");
794
+ assert.strictEqual(sym.return_type, "dict",
795
+ "Expected return_type 'dict', got: " + sym.return_type);
796
+ },
797
+ },
798
+
799
+ {
800
+ name: "infer_return_type_string_literal",
801
+ description: "return 'hello' infers return_type = 'str'",
802
+ run: function () {
803
+ var m = analyze("def get_name():\n return 'hello'");
804
+ var sym = find(m.getAllSymbols(), "get_name");
805
+ assert.ok(sym, "Expected 'get_name' symbol");
806
+ assert.strictEqual(sym.return_type, "str",
807
+ "Expected return_type 'str', got: " + sym.return_type);
808
+ },
809
+ },
810
+
811
+ {
812
+ name: "infer_return_type_number_literal",
813
+ description: "return 42 infers return_type = 'number'",
814
+ run: function () {
815
+ var m = analyze("def get_count():\n return 42");
816
+ var sym = find(m.getAllSymbols(), "get_count");
817
+ assert.ok(sym, "Expected 'get_count' symbol");
818
+ assert.strictEqual(sym.return_type, "number",
819
+ "Expected return_type 'number', got: " + sym.return_type);
820
+ },
821
+ },
822
+
823
+ {
824
+ name: "infer_return_type_bool_literal",
825
+ description: "return True infers return_type = 'bool'",
826
+ run: function () {
827
+ var m = analyze("def is_valid():\n return True");
828
+ var sym = find(m.getAllSymbols(), "is_valid");
829
+ assert.ok(sym, "Expected 'is_valid' symbol");
830
+ assert.strictEqual(sym.return_type, "bool",
831
+ "Expected return_type 'bool', got: " + sym.return_type);
832
+ },
833
+ },
834
+
835
+ {
836
+ name: "infer_return_type_constructor_call",
837
+ description: "return MyClass() infers return_type = 'MyClass'",
838
+ run: function () {
839
+ var m = analyze([
840
+ "class MyClass:",
841
+ " def __init__(self):",
842
+ " pass",
843
+ "def make():",
844
+ " return MyClass()",
845
+ ].join("\n"));
846
+ var sym = find(m.getAllSymbols(), "make");
847
+ assert.ok(sym, "Expected 'make' symbol");
848
+ assert.strictEqual(sym.return_type, "MyClass",
849
+ "Expected return_type 'MyClass', got: " + sym.return_type);
850
+ },
851
+ },
852
+
853
+ {
854
+ name: "infer_return_type_local_variable",
855
+ description: "return local_var infers type from local variable's inferred_class",
856
+ run: function () {
857
+ var m = analyze([
858
+ "def get_items():",
859
+ " result = []",
860
+ " return result",
861
+ ].join("\n"));
862
+ var sym = find(m.getAllSymbols(), "get_items");
863
+ assert.ok(sym, "Expected 'get_items' symbol");
864
+ assert.strictEqual(sym.return_type, "list",
865
+ "Expected return_type 'list' via local var, got: " + sym.return_type);
866
+ },
867
+ },
868
+
869
+ {
870
+ name: "infer_return_type_consistent_multiple_returns",
871
+ description: "Multiple returns of the same type still infer correctly",
872
+ run: function () {
873
+ var m = analyze([
874
+ "def get_name(flag):",
875
+ " if flag:",
876
+ " return 'yes'",
877
+ " return 'no'",
878
+ ].join("\n"));
879
+ var sym = find(m.getAllSymbols(), "get_name");
880
+ assert.ok(sym, "Expected 'get_name' symbol");
881
+ assert.strictEqual(sym.return_type, "str",
882
+ "Expected return_type 'str' for consistent returns, got: " + sym.return_type);
883
+ },
884
+ },
885
+
886
+ {
887
+ name: "infer_return_type_mixed_returns_no_inference",
888
+ description: "Mixed return types produce no inference (return_type stays null)",
889
+ run: function () {
890
+ var m = analyze([
891
+ "def ambiguous(flag):",
892
+ " if flag:",
893
+ " return []",
894
+ " return 'nope'",
895
+ ].join("\n"));
896
+ var sym = find(m.getAllSymbols(), "ambiguous");
897
+ assert.ok(sym, "Expected 'ambiguous' symbol");
898
+ assert.strictEqual(sym.return_type, null,
899
+ "Expected return_type null for mixed types, got: " + sym.return_type);
900
+ },
901
+ },
902
+
903
+ {
904
+ name: "infer_return_type_bare_return_ignored",
905
+ description: "Bare return (no value) does not block inference from typed returns",
906
+ run: function () {
907
+ var m = analyze([
908
+ "def maybe_get(flag):",
909
+ " if not flag:",
910
+ " return",
911
+ " return []",
912
+ ].join("\n"));
913
+ var sym = find(m.getAllSymbols(), "maybe_get");
914
+ assert.ok(sym, "Expected 'maybe_get' symbol");
915
+ assert.strictEqual(sym.return_type, "list",
916
+ "Expected return_type 'list' ignoring bare return, got: " + sym.return_type);
917
+ },
918
+ },
919
+
920
+ {
921
+ name: "infer_return_type_explicit_annotation_not_overwritten",
922
+ description: "An explicit -> annotation is not replaced by inference",
923
+ run: function () {
924
+ var m = analyze("def get_name() -> str:\n return 42");
925
+ var sym = find(m.getAllSymbols(), "get_name");
926
+ assert.ok(sym, "Expected 'get_name' symbol");
927
+ assert.strictEqual(sym.return_type, "str",
928
+ "Explicit annotation should win, got: " + sym.return_type);
929
+ },
930
+ },
931
+
932
+ {
933
+ name: "infer_return_type_nested_function_not_confused",
934
+ description: "Returns inside a nested function do not affect the outer function's type",
935
+ run: function () {
936
+ var m = analyze([
937
+ "def outer():",
938
+ " def inner():",
939
+ " return 'text'",
940
+ " return []",
941
+ ].join("\n"));
942
+ var outer = find(m.getAllSymbols(), "outer");
943
+ var inner = find(m.getAllSymbols(), "inner");
944
+ assert.ok(outer, "Expected 'outer' symbol");
945
+ assert.ok(inner, "Expected 'inner' symbol");
946
+ assert.strictEqual(outer.return_type, "list",
947
+ "outer return_type should be 'list', got: " + outer.return_type);
948
+ assert.strictEqual(inner.return_type, "str",
949
+ "inner return_type should be 'str', got: " + inner.return_type);
950
+ },
951
+ },
952
+
953
+ // ── Nested classes ────────────────────────────────────────────────
954
+
955
+ {
956
+ name: "nested_class_creates_inner_frame",
957
+ description: "A nested class definition creates its own 'class' frame inside the outer class frame",
958
+ run: function () {
959
+ var m = analyze([
960
+ "class Outer:",
961
+ " class Inner:",
962
+ " def method(self):",
963
+ " pass",
964
+ ].join("\n"));
965
+ var cls_frames = m.frames.filter(function (f) { return f.kind === "class"; });
966
+ assert.ok(cls_frames.length >= 2, "Expected at least 2 class frames (Outer + Inner), got " + cls_frames.length);
967
+ var names = cls_frames.map(function (f) { return f.name; });
968
+ assert.ok(names.indexOf("Outer") >= 0, "Expected 'Outer' class frame");
969
+ assert.ok(names.indexOf("Inner") >= 0, "Expected 'Inner' class frame");
970
+ },
971
+ },
972
+
973
+ {
974
+ name: "nested_class_symbol_in_outer_scope",
975
+ description: "The nested class name appears as a 'class' symbol in the outer class frame",
976
+ run: function () {
977
+ var m = analyze([
978
+ "class Outer:",
979
+ " class Inner:",
980
+ " pass",
981
+ ].join("\n"));
982
+ var outer_frame = m.frames.find(function (f) { return f.kind === "class" && f.name === "Outer"; });
983
+ assert.ok(outer_frame, "Expected 'Outer' class frame");
984
+ var inner_sym = outer_frame.getSymbol("Inner");
985
+ assert.ok(inner_sym, "Expected 'Inner' symbol in Outer's class frame");
986
+ assert.strictEqual(inner_sym.kind, "class");
987
+ },
988
+ },
989
+
990
+ {
991
+ name: "nested_class_methods_in_inner_frame",
992
+ description: "Methods of the nested class appear in the inner class frame, not the outer one",
993
+ run: function () {
994
+ var m = analyze([
995
+ "class Outer:",
996
+ " class Inner:",
997
+ " def greet(self):",
998
+ " pass",
999
+ " def do_outer(self):",
1000
+ " pass",
1001
+ ].join("\n"));
1002
+ var inner_frame = m.frames.find(function (f) { return f.kind === "class" && f.name === "Inner"; });
1003
+ assert.ok(inner_frame, "Expected 'Inner' class frame");
1004
+ assert.ok(inner_frame.getSymbol("greet"), "Expected 'greet' in Inner frame");
1005
+ var outer_frame = m.frames.find(function (f) { return f.kind === "class" && f.name === "Outer"; });
1006
+ assert.ok(outer_frame, "Expected 'Outer' class frame");
1007
+ assert.ok(!outer_frame.getSymbol("greet"), "'greet' should NOT be in Outer frame");
1008
+ assert.ok(outer_frame.getSymbol("do_outer"), "Expected 'do_outer' in Outer frame");
1009
+ },
1010
+ },
1011
+
1012
+ {
1013
+ name: "nested_class_appears_in_module_scope",
1014
+ description: "The outer class appears in module scope; the inner class does not pollute module scope",
1015
+ run: function () {
1016
+ var m = analyze([
1017
+ "class Outer:",
1018
+ " class Inner:",
1019
+ " pass",
1020
+ ].join("\n"));
1021
+ var module_frame = m.frames.find(function (f) { return f.kind === "module"; });
1022
+ assert.ok(module_frame, "Expected module frame");
1023
+ assert.ok(module_frame.getSymbol("Outer"), "Expected 'Outer' in module scope");
1024
+ assert.ok(!module_frame.getSymbol("Inner"), "'Inner' should NOT appear directly in module scope");
1025
+ },
1026
+ },
1027
+
773
1028
  ];
774
1029
 
775
1030
  return TESTS;
@@ -763,6 +763,41 @@ function make_tests(Diagnostics, RS) {
763
763
  },
764
764
  },
765
765
 
766
+ {
767
+ name: "slice_builtin_no_undef_error",
768
+ description: "slice() usage produces no 'Undefined symbol' diagnostic",
769
+ run: function () {
770
+ var inst = new Diagnostics(RS);
771
+ var markers = inst.check([
772
+ "s = slice(1, 5)",
773
+ "x = [1, 2, 3, 4, 5]",
774
+ "ok = isinstance(s, slice)",
775
+ ].join("\n"));
776
+ var undef = markers.filter(function (m) {
777
+ return m.message.indexOf("Undefined symbol") !== -1 &&
778
+ m.message.indexOf("slice") !== -1;
779
+ });
780
+ assert.deepStrictEqual(undef, [],
781
+ "Expected no 'Undefined symbol: slice' but got: " + JSON.stringify(undef));
782
+ },
783
+ },
784
+
785
+ {
786
+ name: "list_concat_no_diagnostics",
787
+ description: "list + list and += produce no diagnostic errors",
788
+ run: function () {
789
+ var markers = d().check([
790
+ "a = [1, 2]",
791
+ "b = [3, 4]",
792
+ "c = a + b",
793
+ "a += [5, 6]",
794
+ "print(c)",
795
+ "print(a)",
796
+ ].join("\n"));
797
+ assert_count(markers, SEV_ERROR, 0, "list concat");
798
+ },
799
+ },
800
+
766
801
  ];
767
802
 
768
803
  return TESTS;
@@ -24,6 +24,7 @@ var FILES = [
24
24
  "language-service-hover.js",
25
25
  "language-service-dts.js",
26
26
  "language-service-builtins.js",
27
+ "language-service-index.js",
27
28
  "language-service-bundle.js",
28
29
  "web-repl.js",
29
30
  ];