rapydscript-ng 0.8.2 → 0.8.4

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 (60) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/build.ts +29 -0
  3. package/local-agent.md +4 -0
  4. package/package.json +1 -1
  5. package/release/baselib-plain-pretty.js +85 -61
  6. package/release/baselib-plain-ugly.js +3 -3
  7. package/release/compiler.js +9661 -9540
  8. package/release/signatures.json +26 -26
  9. package/src/ast.pyj +169 -64
  10. package/src/baselib-builtins.pyj +35 -14
  11. package/src/baselib-containers.pyj +70 -40
  12. package/src/baselib-internal.pyj +20 -8
  13. package/src/baselib-itertools.pyj +6 -2
  14. package/src/baselib-str.pyj +74 -28
  15. package/src/errors.pyj +4 -1
  16. package/src/lib/aes.pyj +664 -35
  17. package/src/lib/elementmaker.pyj +105 -13
  18. package/src/lib/encodings.pyj +31 -7
  19. package/src/lib/gettext.pyj +6 -3
  20. package/src/lib/math.pyj +38 -22
  21. package/src/lib/pythonize.pyj +5 -3
  22. package/src/lib/random.pyj +13 -5
  23. package/src/lib/re.pyj +81 -18
  24. package/src/lib/traceback.pyj +42 -10
  25. package/src/lib/uuid.pyj +2 -1
  26. package/src/output/classes.pyj +70 -26
  27. package/src/output/codegen.pyj +92 -20
  28. package/src/output/comments.pyj +11 -4
  29. package/src/output/exceptions.pyj +17 -4
  30. package/src/output/functions.pyj +145 -26
  31. package/src/output/literals.pyj +7 -2
  32. package/src/output/loops.pyj +79 -23
  33. package/src/output/modules.pyj +41 -12
  34. package/src/output/operators.pyj +154 -31
  35. package/src/output/statements.pyj +38 -10
  36. package/src/output/stream.pyj +43 -12
  37. package/src/output/utils.pyj +10 -3
  38. package/src/parse.pyj +740 -259
  39. package/src/string_interpolation.pyj +4 -1
  40. package/src/tokenizer.pyj +314 -57
  41. package/src/unicode_aliases.pyj +4 -2
  42. package/src/utils.pyj +19 -5
  43. package/test/functions.pyj +8 -0
  44. package/test/generic.pyj +9 -0
  45. package/test/lsp.pyj +16 -0
  46. package/test/str.pyj +10 -0
  47. package/tools/cli.mjs +4 -3
  48. package/tools/compile.mjs +9 -3
  49. package/tools/compiler.mjs +0 -6
  50. package/tools/fmt.mjs +41 -2
  51. package/tools/lint-worker.mjs +107 -0
  52. package/tools/lint.mjs +134 -8
  53. package/tools/lsp.mjs +11 -1
  54. package/tree-sitter/grammar.js +27 -6
  55. package/tree-sitter/queries/highlights.scm +10 -0
  56. package/tree-sitter/src/grammar.json +130 -5
  57. package/tree-sitter/src/node-types.json +73 -0
  58. package/tree-sitter/src/parser.c +119589 -114993
  59. package/tree-sitter/src/scanner.c +275 -17
  60. package/tree-sitter/test/corpus/strings.txt +41 -5
package/src/utils.pyj CHANGED
@@ -2,7 +2,8 @@
2
2
  # License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
3
3
  from __python__ import hash_literals
4
4
 
5
- has_prop = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty)
5
+ has_prop = Object.prototype.hasOwnProperty.call.bind(
6
+ Object.prototype.hasOwnProperty)
6
7
 
7
8
 
8
9
  def array_to_hash(a):
@@ -41,7 +42,11 @@ def repeat_string(str_, i):
41
42
 
42
43
  class DefaultsError(ValueError):
43
44
  def __init__(self, name, defs):
44
- ValueError.__init__(self, name + ' is not a supported option. Supported options are: ' + str(Object.keys(defs)))
45
+ ValueError.__init__(
46
+ self,
47
+ name + ' is not a supported option. Supported options are: '
48
+ + str(Object.keys(defs))
49
+ )
45
50
 
46
51
 
47
52
  def defaults(args, defs, croak):
@@ -80,12 +85,18 @@ MAP = def ():
80
85
  if isinstance(val, AtTop):
81
86
  val = val.v
82
87
  if isinstance(val, Splice):
83
- top.push.apply(top, (val.v.slice().reverse() if backwards else val.v))
88
+ top.push.apply(
89
+ top,
90
+ (val.v.slice().reverse() if backwards else val.v)
91
+ )
84
92
  else:
85
93
  top.push(val)
86
94
  elif val is not skip:
87
95
  if isinstance(val, Splice):
88
- ret.push.apply(ret, (val.v.slice().reverse() if backwards else val.v))
96
+ ret.push.apply(
97
+ ret,
98
+ (val.v.slice().reverse() if backwards else val.v)
99
+ )
89
100
  else:
90
101
  ret.push(val)
91
102
  return is_last
@@ -205,5 +216,8 @@ def make_predicate(words):
205
216
  def cache_file_name(src, cache_dir):
206
217
  if cache_dir:
207
218
  src = str.replace(src, '\\', '/')
208
- return cache_dir + '/' + str.lstrip(str.replace(src, '/', '-') + '.bin', '-')
219
+ return cache_dir + '/' + str.lstrip(
220
+ str.replace(src, '/', '-') + '.bin',
221
+ '-'
222
+ )
209
223
  return src + '-cached'
@@ -22,6 +22,14 @@ mul = None
22
22
  assrt.equal(add(1,2), 3)
23
23
  assrt.equal(sub(1,2), -1)
24
24
  assrt.equal(mul(2,2), 4)
25
+
26
+ # newline as implicit comma separator in function calls
27
+ assrt.equal(add(1
28
+ 2), 3)
29
+ assrt.equal(sub(1
30
+ 2), -1)
31
+ assrt.equal(add(list().length
32
+ list().length), 0)
25
33
  # for some reason input to throws must be of type block, hence the 'def' wrapper
26
34
  assrt.throws(
27
35
  def():
package/test/generic.pyj CHANGED
@@ -48,6 +48,15 @@ assrt.deepEqual((1,
48
48
  assrt.deepEqual((1,
49
49
  2), [1, 2])
50
50
 
51
+ # Tuples with newlines as implicit separators (no comma)
52
+ assrt.deepEqual((1
53
+ 2), [1, 2])
54
+ assrt.deepEqual((list()
55
+ list()), [[], []])
56
+ assrt.deepEqual((1
57
+ 2
58
+ 3), [1, 2, 3])
59
+
51
60
  # Conditional operators
52
61
  assrt.equal(1 if True else 2, 1)
53
62
  assrt.equal(1
package/test/lsp.pyj CHANGED
@@ -50,6 +50,22 @@ async def test_diagnostics():
50
50
  assrt.ok(c['import-unresolved'], 'expected import-unresolved for nonexistent_module')
51
51
  fs.rmSync(tmp, {'recursive': True})
52
52
 
53
+ async def test_globals_directive_suppresses_undef():
54
+ tmp = write_workspace()
55
+ ctx = make_ctx(tmp)
56
+ uri = uri_of(path.join(tmp, 'globals_test.pyj'))
57
+ # ext_func is declared via # globals: so it must not produce an undef diagnostic.
58
+ text = '# globals: ext_func\n\nresult = ext_func(42)\n'
59
+ diags = await lsp.compute_diagnostics(ctx, uri, text)
60
+ c = codes(diags)
61
+ assrt.ok(not c['undef'], 'ext_func declared in # globals: must not be flagged as undef')
62
+ # A name NOT in # globals: must still be flagged.
63
+ text2 = '# globals: ext_func\n\nresult = ext_func(42)\nbad = undeclared_thing()\n'
64
+ diags2 = await lsp.compute_diagnostics(ctx, uri, text2)
65
+ c2 = codes(diags2)
66
+ assrt.ok(c2['undef'], 'undeclared_thing not in # globals: must still be flagged as undef')
67
+ fs.rmSync(tmp, {'recursive': True})
68
+
53
69
  async def test_hover_and_definition():
54
70
  tmp = write_workspace()
55
71
  ctx = make_ctx(tmp)
package/test/str.pyj CHANGED
@@ -65,6 +65,16 @@ def test_interpolation():
65
65
  ae('a' f'{a}' 'b', 'a1b')
66
66
  ae('a' f'{a}' f'{a}', 'a11')
67
67
  ae(rf'raw{a}' f' and {a}', 'raw1 and 1')
68
+ # Test f-string + string concatenation across newlines inside brackets
69
+ ae(f'{a}'
70
+ "b", '1b')
71
+ ae(f'{a}'
72
+ f'{a}', '11')
73
+ ae('a'
74
+ f'{a}', 'a1')
75
+ ae(f'{a}'
76
+ "b"
77
+ f'{a}', '1b1')
68
78
 
69
79
  somevar = 33
70
80
  test('somevar=33', '{somevar=}', somevar=somevar)
package/tools/cli.mjs CHANGED
@@ -318,11 +318,12 @@ command prompt.`);
318
318
  opt("no_js", '', 'bool', false, `Do not display the compiled JavaScript before executing
319
319
  it.`);
320
320
 
321
- create_group('lint', "[input1.pyj input2.pyj ...]", `Run the RapydScript linter. This will find various
321
+ create_group('lint', "[input1.pyj dir1 ...]", `Run the RapydScript linter. This will find various
322
322
  possible problems in the .pyj files you specify and
323
323
  write messages about them to stdout. Use - to read from STDIN.
324
- The main check it performs is for unused/undefined
325
- symbols, like pyflakes does for python.`,
324
+ You can specify directories to recursively lint all .pyj
325
+ files within them. The main check it performs is for
326
+ unused/undefined symbols, like pyflakes does for python.`,
326
327
  `In addition to the command line options listed below,
327
328
  you can also control the linter in a couple of other ways.
328
329
 
package/tools/compile.mjs CHANGED
@@ -111,6 +111,12 @@ export default async function(start_time, argv, base_path, src_path, lib_path) {
111
111
  });
112
112
  }
113
113
 
114
+ function write_to_stream(stream, data) {
115
+ return new Promise((resolve, reject) => {
116
+ stream.write(data + '\n', 'utf8', err => err ? reject(err) : resolve());
117
+ });
118
+ }
119
+
114
120
  async function write_output(js_output, output_stream) {
115
121
  if (argv.source_map && output_stream) {
116
122
  var segments = output_stream.get_source_map_segments();
@@ -123,11 +129,11 @@ export default async function(start_time, argv, base_path, src_path, lib_path) {
123
129
  }
124
130
  if (argv.output) {
125
131
  // Node's filesystem module cannot write directly to /dev/stdout
126
- if (argv.output == '/dev/stdout') console.log(js_output);
127
- else if (argv.output == '/dev/stderr') console.error(js_output);
132
+ if (argv.output == '/dev/stdout') await write_to_stream(process.stdout, js_output);
133
+ else if (argv.output == '/dev/stderr') await write_to_stream(process.stderr, js_output);
128
134
  else await fs.promises.writeFile(argv.output, js_output, "utf8");
129
135
  } else if (!argv.execute){
130
- console.log(js_output);
136
+ await write_to_stream(process.stdout, js_output);
131
137
  }
132
138
  if (argv.execute) {
133
139
  vm.runInNewContext(js_output, {'console':console, 'require':require}, {'filename':files[0]});
@@ -13,7 +13,6 @@ import path from 'path';
13
13
  import fs from 'fs';
14
14
  import crypto from 'crypto';
15
15
  import vm from 'vm';
16
- import * as terser from 'terser';
17
16
  import { fileURLToPath } from 'url';
18
17
  import { createRequire } from 'module';
19
18
  import { generate_source_map } from './sourcemap.mjs';
@@ -39,11 +38,6 @@ async function path_exists(p) {
39
38
  }
40
39
  }
41
40
 
42
- function uglify(code) {
43
- var ans = terser.minify_sync(code);
44
- if (ans.error) throw ans.error;
45
- return ans.code;
46
- }
47
41
 
48
42
 
49
43
  async function find_compiler_dir() {
package/tools/fmt.mjs CHANGED
@@ -848,13 +848,51 @@ function normalize_opts(options) {
848
848
  // check-only reporting
849
849
  // ---------------------------------------------------------------------------
850
850
 
851
+ // Returns a Set of 0-based line indices that fall inside vr/v verbatim
852
+ // triple-quoted blocks. The formatter cannot reflow that content (it is
853
+ // raw JS/text), so line-length violations inside those blocks are skipped.
854
+ function verbatim_line_set(lines) {
855
+ var skip = new Set();
856
+ var in_block = false;
857
+ var close_q = null;
858
+ for (var i = 0; i < lines.length; i++) {
859
+ var line = lines[i];
860
+ if (!in_block) {
861
+ // Detect the opening of a verbatim triple-quoted block.
862
+ // Patterns: vr''', vr""", v''', v""" (with any combination of
863
+ // v/r/u/f prefix letters, case-insensitive).
864
+ var m = /\b[vrufVRUF]*(?:'{3}|"{3})/.exec(line);
865
+ if (m) {
866
+ var q = m[0].slice(-3); // ''' or """
867
+ var after = line.slice(m.index + m[0].length);
868
+ if (after.indexOf(q) < 0) {
869
+ // Block does not close on the same line.
870
+ in_block = true;
871
+ close_q = q;
872
+ skip.add(i); // opening line itself may be long
873
+ }
874
+ // If it closes on the same line we leave skip alone —
875
+ // the single-line v'...' case is handled per-line below.
876
+ }
877
+ } else {
878
+ skip.add(i);
879
+ if (line.indexOf(close_q) >= 0) {
880
+ in_block = false;
881
+ close_q = null;
882
+ }
883
+ }
884
+ }
885
+ return skip;
886
+ }
887
+
851
888
  export function check_report(file, src, formatted, options) {
852
889
  var opts = normalize_opts(options);
853
890
  var errs = [];
854
891
  if (formatted !== src.replace(/\r\n?/g, '\n')) errs.push(file + ': would be reformatted');
855
892
  var lines = formatted.split('\n');
893
+ var skip = verbatim_line_set(lines);
856
894
  for (var i = 0; i < lines.length; i++) {
857
- if (lines[i].length > opts.line_length) {
895
+ if (!skip.has(i) && lines[i].length > opts.line_length) {
858
896
  errs.push(file + ':' + (i + 1) + ': line exceeds ' + opts.line_length + ' characters (' + lines[i].length + ')');
859
897
  }
860
898
  }
@@ -970,8 +1008,9 @@ export async function cli(argv, base_path, src_path, lib_path) {
970
1008
  console.log('reformatted ' + f);
971
1009
  }
972
1010
  var flines = formatted.split('\n');
1011
+ var fskip = verbatim_line_set(flines);
973
1012
  for (var li = 0; li < flines.length; li++) {
974
- if (flines[li].length > opts.line_length) {
1013
+ if (!fskip.has(li) && flines[li].length > opts.line_length) {
975
1014
  console.error(f + ':' + (li + 1) + ': line exceeds ' + opts.line_length + ' characters (' + flines[li].length + ')');
976
1015
  had_errors = true;
977
1016
  }
@@ -0,0 +1,107 @@
1
+ /* vim:fileencoding=utf-8
2
+ *
3
+ * Copyright (C) 2015 Kovid Goyal <kovid at kovidgoyal.net>
4
+ *
5
+ * Distributed under terms of the BSD license
6
+ *
7
+ * Worker thread for parallel linting. Each worker owns its own compiler
8
+ * instance so parsing can run on multiple CPU cores simultaneously.
9
+ */
10
+
11
+ import { parentPort, workerData } from 'worker_threads';
12
+ import { create_compiler } from './compiler.mjs';
13
+ import { read_config } from './ini.mjs';
14
+ import * as utils from './utils.mjs';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+
18
+ const merge = utils.merge;
19
+ const has_prop = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
20
+
21
+ // Restore embedded assets forwarded by the main thread (needed in compiled binaries
22
+ // where workers don't inherit the main thread's globalThis).
23
+ if (workerData && workerData.embedded) {
24
+ globalThis.__rapydscript_embedded__ = workerData.embedded;
25
+ }
26
+
27
+ // Initialize the compiler first — lint.mjs reads globalThis.create_rapydscript_compiler
28
+ // at module evaluation time, so the global must be set before the dynamic import.
29
+ const compiler = await create_compiler();
30
+ globalThis.create_rapydscript_compiler = () => compiler;
31
+
32
+ // Safe to import now.
33
+ const { lint_code } = await import('./lint.mjs');
34
+
35
+ // Per-worker ini cache keyed by directory. Stores promises to deduplicate
36
+ // concurrent lookups for the same directory.
37
+ const ini_cache = {};
38
+
39
+ function get_ini(dir) {
40
+ if (!has_prop(ini_cache, dir)) {
41
+ ini_cache[dir] = read_config(dir).then(r => r.rapydscript || {});
42
+ }
43
+ return ini_cache[dir];
44
+ }
45
+
46
+ // Signal to the main thread that this worker is ready to receive jobs.
47
+ parentPort.postMessage({ type: 'ready' });
48
+
49
+ parentPort.on('message', async (msg) => {
50
+ if (msg.type !== 'lint') return;
51
+ const { id, filename, base_builtins, base_noqa } = msg;
52
+
53
+ try {
54
+ const code = await fs.promises.readFile(filename, 'utf-8');
55
+
56
+ const final_builtins = merge(base_builtins);
57
+ const final_noqa = merge(base_noqa);
58
+
59
+ const rl = await get_ini(path.dirname(filename));
60
+ const g = {};
61
+ (rl.globals || rl.builtins || '').split(',').forEach(function(x) { g[x.trim()] = true; });
62
+ Object.assign(final_builtins, g);
63
+
64
+ const ng = {};
65
+ (rl.noqa || '').split(',').forEach(function(x) { ng[x.trim()] = true; });
66
+ Object.assign(final_noqa, ng);
67
+
68
+ code.split('\n', 20).forEach(function(line) {
69
+ var lq = line.replace(/\s+/g, '');
70
+ if (lq.startsWith('#globals:')) {
71
+ (lq.split(':', 2)[1] || '').split(',').forEach(function(item) { final_builtins[item] = true; });
72
+ } else if (lq.startsWith('#noqa:')) {
73
+ (lq.split(':', 2)[1] || '').split(',').forEach(function(item) { final_noqa[item] = true; });
74
+ }
75
+ });
76
+
77
+ // Suppress per-file output; the main thread reports in file order.
78
+ const messages = await lint_code(code, {
79
+ filename,
80
+ builtins: final_builtins,
81
+ noqa: final_noqa,
82
+ errorformat: false,
83
+ report: function() {},
84
+ });
85
+
86
+ // Strip code_lines — it is never used by the CLI report functions and
87
+ // transferring it across threads would be wasteful.
88
+ const clean = messages.map(function(m) {
89
+ return {
90
+ filename: m.filename,
91
+ start_line: m.start_line,
92
+ start_col: m.start_col,
93
+ end_line: m.end_line,
94
+ end_col: m.end_col,
95
+ ident: m.ident,
96
+ message: m.message,
97
+ level: m.level,
98
+ name: m.name,
99
+ other_line: m.other_line,
100
+ };
101
+ });
102
+
103
+ parentPort.postMessage({ type: 'result', id, messages: clean });
104
+ } catch (e) {
105
+ parentPort.postMessage({ type: 'error', id, error: e.message || String(e) });
106
+ }
107
+ });
package/tools/lint.mjs CHANGED
@@ -663,11 +663,12 @@ function cli_vim_report(r) {
663
663
 
664
664
  var ini_cache = {};
665
665
 
666
- async function get_ini(toplevel_dir) {
667
- if (has_prop(ini_cache, toplevel_dir)) return ini_cache[toplevel_dir];
668
- var rl = (await read_config(toplevel_dir)).rapydscript || {};
669
- ini_cache[toplevel_dir] = rl;
670
- return rl;
666
+ function get_ini(toplevel_dir) {
667
+ if (!has_prop(ini_cache, toplevel_dir)) {
668
+ // Store the promise so concurrent callers for the same dir share one read.
669
+ ini_cache[toplevel_dir] = read_config(toplevel_dir).then(function(r) { return r.rapydscript || {}; });
670
+ }
671
+ return ini_cache[toplevel_dir];
671
672
  }
672
673
 
673
674
  export async function cli(argv, base_path, src_path, lib_path) {
@@ -716,7 +717,52 @@ export async function cli(argv, base_path, src_path, lib_path) {
716
717
  return x === '-' ? argv.stdin_filename : x;
717
718
  }
718
719
 
719
- for (const filename of (files.length ? files : [null])) {
720
+ // reportcb used for final output (same logic as inside lint_code)
721
+ var reportcb = {'json': cli_json_report, 'vim': cli_vim_report, 'undef': cli_undef_report}[argv.errorformat] || cli_report;
722
+
723
+ // Expand directories to .pyj files recursively, stat-ing entries in parallel.
724
+ async function expand_dirs(inputs, toplevel) {
725
+ // Stat all entries concurrently, then process results in sorted order.
726
+ var entries = await Promise.all(inputs.map(async function(f) {
727
+ if (f === '-') return { f, type: 'stdin' };
728
+ var st;
729
+ try { st = await fs.promises.lstat(f); }
730
+ catch (e) {
731
+ if (toplevel) { console.error("ERROR: can't access: " + f); process.exit(1); }
732
+ return null;
733
+ }
734
+ if (st.isDirectory()) return { f, type: 'dir' };
735
+ if (st.isFile()) return { f, type: 'file' };
736
+ return null;
737
+ }));
738
+
739
+ var result = [];
740
+ for (var i = 0; i < entries.length; i++) {
741
+ var entry = entries[i];
742
+ if (!entry) continue;
743
+ if (entry.type === 'stdin') { result.push(entry.f); continue; }
744
+ if (entry.type === 'file') { result.push(entry.f); continue; }
745
+ // Directory: read children and recurse
746
+ var children;
747
+ try {
748
+ children = (await fs.promises.readdir(entry.f)).sort().map(function(x) { return path.join(entry.f, x); });
749
+ } catch(e) {
750
+ if (toplevel) { console.error("ERROR: can't read directory: " + entry.f); process.exit(1); }
751
+ continue;
752
+ }
753
+ var sub = await expand_dirs(children, false);
754
+ sub.forEach(function(x) { if (x.endsWith('.pyj')) result.push(x); });
755
+ }
756
+ return result;
757
+ }
758
+ if (files.length) files = await expand_dirs(files, true);
759
+
760
+ var all_files = files.length ? files : [null];
761
+ var has_stdin = all_files.indexOf('-') !== -1 || all_files.indexOf(null) !== -1;
762
+
763
+ // Process a single file and return its messages without printing them.
764
+ // Used when the worker pool is not active (stdin, or too few files).
765
+ async function process_file(filename) {
720
766
  var code;
721
767
  try {
722
768
  code = await read_whole_file(filename === '-' ? null : filename);
@@ -747,10 +793,90 @@ export async function cli(argv, base_path, src_path, lib_path) {
747
793
  }
748
794
  });
749
795
 
750
- // Lint!
751
- if ((await lint_code(code, {filename:path_for_filename(filename || '-'), builtins:final_builtins, noqa:final_noqa, errorformat:argv.errorformat || false})).length) all_ok = false;
796
+ var lines = code.split('\n');
797
+ var msgs = await lint_code(code, {filename:path_for_filename(filename || '-'), builtins:final_builtins, noqa:final_noqa, errorformat:argv.errorformat || false, report:function() {}});
798
+ msgs.forEach(function(msg) { msg.code_lines = lines; });
799
+ return msgs;
800
+ }
801
+
802
+ var all_results;
803
+ // Use a worker pool when there are enough files to justify the overhead.
804
+ // Workers own their own compiler instance so parsing runs in parallel across CPU cores.
805
+ var WORKER_THRESHOLD = 4;
806
+
807
+ // In compiled binaries lint.mjs is dynamically imported so Bun's static
808
+ // analysis never sees the new Worker() call. build.ts pre-bundles
809
+ // lint-worker.mjs and embeds it via `with { type: 'file' }`, storing the
810
+ // virtual-FS path in __rapydscript_embedded__['lint-worker-url'].
811
+ // Fall back gracefully (single-threaded) if the URL isn't available.
812
+ var embedded = globalThis.__rapydscript_embedded__;
813
+ var worker_url = embedded
814
+ ? embedded['lint-worker-url']
815
+ : new URL('./lint-worker.mjs', import.meta.url);
816
+
817
+ if (!has_stdin && all_files.length >= WORKER_THRESHOLD && worker_url) {
818
+ var { Worker } = await import('worker_threads');
819
+ var os_mod = await import('os');
820
+ var num_workers = Math.min(os_mod.cpus().length, all_files.length, 8);
821
+
822
+ // Dispatch files to workers dynamically so fast files don't leave workers idle.
823
+ var ordered_results = new Array(all_files.length);
824
+ var next_idx = 0;
825
+ var pending = all_files.length;
826
+ var done_resolve;
827
+ var done_promise = new Promise(function(res) { done_resolve = res; });
828
+ if (pending === 0) done_resolve();
829
+
830
+ function dispatch(worker) {
831
+ if (next_idx >= all_files.length) return;
832
+ var idx = next_idx++;
833
+ worker.postMessage({
834
+ type: 'lint',
835
+ id: idx,
836
+ filename: all_files[idx],
837
+ base_builtins: builtins,
838
+ base_noqa: noqa,
839
+ });
840
+ }
841
+
842
+ // Create workers and dispatch eagerly as each becomes ready rather than
843
+ // waiting for all workers before starting any work.
844
+ var worker_promises = Array.from({length: num_workers}, function() {
845
+ return new Promise(function(resolve) {
846
+ var w = new Worker(worker_url, { workerData: { embedded: embedded || null } });
847
+ w.once('message', function(m) {
848
+ if (m.type !== 'ready') return;
849
+ w.on('message', function(msg) {
850
+ if (msg.type === 'result') {
851
+ ordered_results[msg.id] = msg.messages;
852
+ } else if (msg.type === 'error') {
853
+ console.error("ERROR: lint failed for " + all_files[msg.id] + ": " + msg.error);
854
+ ordered_results[msg.id] = [];
855
+ }
856
+ pending--;
857
+ if (pending === 0) { done_resolve(); return; }
858
+ dispatch(w);
859
+ });
860
+ dispatch(w);
861
+ resolve(w);
862
+ });
863
+ });
864
+ });
865
+
866
+ await done_promise;
867
+ var workers = await Promise.all(worker_promises);
868
+ workers.forEach(function(w) { w.terminate(); });
869
+ all_results = ordered_results;
870
+ } else {
871
+ // Single-threaded path: process concurrently on the main thread.
872
+ all_results = await Promise.all(all_files.map(process_file));
752
873
  }
753
874
 
875
+ var all_messages = [];
876
+ all_results.forEach(function(msgs) { Array.prototype.push.apply(all_messages, msgs); });
877
+ if (all_messages.length) all_ok = false;
878
+ all_messages.forEach(function(msg, i) { reportcb(msg, i, all_messages); });
879
+
754
880
  process.exit((all_ok) ? 0 : 1);
755
881
  }
756
882
  // }}}
package/tools/lsp.mjs CHANGED
@@ -230,7 +230,7 @@ export async function compute_diagnostics(ctx, uri, raw_text) {
230
230
  // 2. Linter diagnostics -- only when the file parsed cleanly, so we do not
231
231
  // emit misleading undefined/unused warnings from a partial AST.
232
232
  if (a.toplevel && (!a.recovered_errors || a.recovered_errors.length === 0)) {
233
- var builtins = utils.merge(BUILTINS);
233
+ var builtins = utils.merge(BUILTINS, file_globals(a.text));
234
234
  var messages = lint_parsed(a.toplevel, a.text, { filename: a.file_path, builtins: builtins, noqa: file_noqa(a.text) });
235
235
  messages.forEach(function (m) {
236
236
  out.push(lint_message_to_diagnostic(doc, m));
@@ -265,6 +265,16 @@ function file_noqa(code) {
265
265
  return noqa;
266
266
  }
267
267
 
268
+ // Parse `# globals: a,b` file-level directives from the first lines (mirrors lint cli).
269
+ function file_globals(code) {
270
+ var globals = Object.create(null);
271
+ code.split('\n', 20).forEach(function (line) {
272
+ var lq = line.replace(/\s+/g, '');
273
+ if (lq.slice(0, 9).toLowerCase() === '#globals:') (lq.split(':', 2)[1] || '').split(',').forEach(function (x) { if (x) globals[x] = true; });
274
+ });
275
+ return globals;
276
+ }
277
+
268
278
  function lint_message_to_diagnostic(doc, m) {
269
279
  var start_line = (m.start_line || 1) - 1;
270
280
  var start_col = (m.start_col === undefined || m.start_col === null) ? 0 : m.start_col;
@@ -57,6 +57,9 @@ module.exports = grammar({
57
57
  $._indent,
58
58
  $._dedent,
59
59
  $.regex,
60
+ $.fstring_start,
61
+ $.fstring_content,
62
+ $.fstring_end,
60
63
  ],
61
64
 
62
65
  extras: $ => [
@@ -589,6 +592,7 @@ module.exports = grammar({
589
592
  $.none,
590
593
  $.number,
591
594
  $.string,
595
+ $.f_string,
592
596
  $.concatenated_string,
593
597
  $.regex,
594
598
  $.verbatim,
@@ -924,20 +928,37 @@ module.exports = grammar({
924
928
  // ---- atoms / literals ------------------------------------------------
925
929
 
926
930
  concatenated_string: $ => prec.left(seq(
927
- $.string,
928
- repeat1($.string),
931
+ choice($.string, $.f_string),
932
+ repeat1(choice($.string, $.f_string)),
929
933
  )),
930
934
 
935
+ f_string: $ => seq(
936
+ $.fstring_start,
937
+ repeat(choice(
938
+ $.fstring_content,
939
+ $.interpolation,
940
+ )),
941
+ $.fstring_end,
942
+ ),
943
+
944
+ interpolation: $ => seq(
945
+ '{',
946
+ field('expression', $._expression),
947
+ optional(field('type_conversion', /![rsa]/)),
948
+ optional(seq(':', field('format_spec', $.fstring_content))),
949
+ '}',
950
+ ),
951
+
931
952
  this: _ => 'this',
932
953
  true: _ => 'True',
933
954
  false: _ => 'False',
934
955
  none: _ => 'None',
935
956
 
936
- // string with optional r/u/f/b modifiers (the `v` modifier denotes a
937
- // verbatim JavaScript literal and is handled separately). Interpolation
938
- // and escapes are not sub-tokenised; the whole literal is a single node.
957
+ // string with optional r/u/b modifiers (the `v` modifier denotes a
958
+ // verbatim JavaScript literal, handled separately; the `f` modifier
959
+ // produces an f_string with structured interpolation nodes).
939
960
  string: _ => token(seq(
940
- optional(/[rRuUfFbB]+/),
961
+ optional(/[rRuUbB]+/),
941
962
  choice(
942
963
  seq('"""', repeat(choice(/[^"\\]/, /\\(.|\n)/, /"[^"]/, /""[^"]/)), '"""'),
943
964
  seq("'''", repeat(choice(/[^'\\]/, /\\(.|\n)/, /'[^']/, /''[^']/)), "'''"),
@@ -21,6 +21,16 @@
21
21
  (regex) @string.regexp
22
22
  (number) @number
23
23
 
24
+ ; f-strings: delimiters and literal content get @string, interpolation braces
25
+ ; get @punctuation.special so they stand out, and the expression inside is
26
+ ; transparent — its child nodes (identifiers, calls, etc.) are highlighted by
27
+ ; the normal rules below.
28
+ (fstring_start) @string
29
+ (fstring_content) @string
30
+ (fstring_end) @string
31
+ (interpolation "{" @punctuation.special)
32
+ (interpolation "}" @punctuation.special)
33
+
24
34
  (true) @boolean
25
35
  (false) @boolean
26
36
  (none) @constant.builtin