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.
- package/CHANGELOG.md +17 -0
- package/bin/build.ts +29 -0
- package/local-agent.md +4 -0
- package/package.json +1 -1
- package/release/baselib-plain-pretty.js +85 -61
- package/release/baselib-plain-ugly.js +3 -3
- package/release/compiler.js +9661 -9540
- package/release/signatures.json +26 -26
- package/src/ast.pyj +169 -64
- package/src/baselib-builtins.pyj +35 -14
- package/src/baselib-containers.pyj +70 -40
- package/src/baselib-internal.pyj +20 -8
- package/src/baselib-itertools.pyj +6 -2
- package/src/baselib-str.pyj +74 -28
- package/src/errors.pyj +4 -1
- package/src/lib/aes.pyj +664 -35
- package/src/lib/elementmaker.pyj +105 -13
- package/src/lib/encodings.pyj +31 -7
- package/src/lib/gettext.pyj +6 -3
- package/src/lib/math.pyj +38 -22
- package/src/lib/pythonize.pyj +5 -3
- package/src/lib/random.pyj +13 -5
- package/src/lib/re.pyj +81 -18
- package/src/lib/traceback.pyj +42 -10
- package/src/lib/uuid.pyj +2 -1
- package/src/output/classes.pyj +70 -26
- package/src/output/codegen.pyj +92 -20
- package/src/output/comments.pyj +11 -4
- package/src/output/exceptions.pyj +17 -4
- package/src/output/functions.pyj +145 -26
- package/src/output/literals.pyj +7 -2
- package/src/output/loops.pyj +79 -23
- package/src/output/modules.pyj +41 -12
- package/src/output/operators.pyj +154 -31
- package/src/output/statements.pyj +38 -10
- package/src/output/stream.pyj +43 -12
- package/src/output/utils.pyj +10 -3
- package/src/parse.pyj +740 -259
- package/src/string_interpolation.pyj +4 -1
- package/src/tokenizer.pyj +314 -57
- package/src/unicode_aliases.pyj +4 -2
- package/src/utils.pyj +19 -5
- package/test/functions.pyj +8 -0
- package/test/generic.pyj +9 -0
- package/test/lsp.pyj +16 -0
- package/test/str.pyj +10 -0
- package/tools/cli.mjs +4 -3
- package/tools/compile.mjs +9 -3
- package/tools/compiler.mjs +0 -6
- package/tools/fmt.mjs +41 -2
- package/tools/lint-worker.mjs +107 -0
- package/tools/lint.mjs +134 -8
- package/tools/lsp.mjs +11 -1
- package/tree-sitter/grammar.js +27 -6
- package/tree-sitter/queries/highlights.scm +10 -0
- package/tree-sitter/src/grammar.json +130 -5
- package/tree-sitter/src/node-types.json +73 -0
- package/tree-sitter/src/parser.c +119589 -114993
- package/tree-sitter/src/scanner.c +275 -17
- 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(
|
|
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__(
|
|
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(
|
|
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(
|
|
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(
|
|
219
|
+
return cache_dir + '/' + str.lstrip(
|
|
220
|
+
str.replace(src, '/', '-') + '.bin',
|
|
221
|
+
'-'
|
|
222
|
+
)
|
|
209
223
|
return src + '-cached'
|
package/test/functions.pyj
CHANGED
|
@@ -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
|
|
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
|
-
|
|
325
|
-
|
|
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')
|
|
127
|
-
else if (argv.output == '/dev/stderr')
|
|
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
|
-
|
|
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]});
|
package/tools/compiler.mjs
CHANGED
|
@@ -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
|
-
|
|
667
|
-
if (has_prop(ini_cache, toplevel_dir))
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
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
|
-
|
|
751
|
-
|
|
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;
|
package/tree-sitter/grammar.js
CHANGED
|
@@ -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/
|
|
937
|
-
// verbatim JavaScript literal
|
|
938
|
-
//
|
|
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(/[
|
|
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
|