1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
// Per-language metric and AST modules deliberately consume the macro-
// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
// inside match expressions — explicit imports would list dozens of
// variants per arm and obscure the per-language token sets that are the
// point of these files. Allowed at the module level rather than per
// function so the per-language impl blocks stay readable.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
//! The [`Getter`] accessors — names, space kinds, Halstead classes — and
//! their per-language impls.
use crate::token_role::TokenRole;
use crate::lang_helpers::tcl_family::{
ArgumentRoles, Dialect, SWITCH_COMMAND, argument_slots_are_readable, fills_script_slot,
namespace_script_slots, script_slots,
};
use crate::space_kind::SpaceKind;
use crate::traits::Search;
use generic_angles::{GenericAngleKinds, never_misparsed};
use js_family::impl_js_family_get_op_type;
use crate::*;
/// Bounds- and UTF-8-checked text extraction for a node's byte span.
///
/// `code` is `&[u8]`, so slicing it carries no char-boundary
/// precondition; the two guards here cover two unrelated failure modes:
///
/// * `std::str::from_utf8` rejects non-UTF-8 bytes. This one is
/// reachable from ordinary use — `big_code_analysis::Ast::parse` accepts
/// arbitrary bytes, so a node span in a partially-binary source need
/// not be valid UTF-8.
/// * `code.get` bounds-checks the range. This one is reachable only by
/// violating the same-parse precondition documented on `Getter`, i.e.
/// `big_code_analysis::Ast::from_tree_sitter` adopting a tree built from longer
/// source than the `code` passed alongside it.
///
/// Both degrade to `None`. The walker stores a space's name as
/// `Option<String>` (`spaces.rs`), so that records an unnamed space
/// rather than crashing — note this is *not* the same path as a node
/// with no `name` field, which `get_func_space_name` reports as
/// `Some("<anonymous>")` without reaching here. The unguarded sibling
/// slice sites return infallible
/// types feeding metric arithmetic or rendered output; there the only
/// available fallback would be a fabricated empty value that silently
/// corrupts a count, so they rely on the precondition instead. The
/// asymmetry is deliberate: guarding is free here because `Option` is
/// already part of this signature's contract (#1059).
#[inline]
pub(crate) fn node_text<'a>(code: &'a [u8], node: &Node) -> Option<&'a str> {
code.get(node.start_byte()..node.end_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
}
macro_rules! get_operator {
($language:ident) => {
#[inline]
fn get_operator_id_as_str(id: u16) -> &'static str {
let typ = id.into();
match typ {
$language::LPAREN => "()",
$language::LBRACK => "[]",
$language::LBRACE => "{}",
_ => typ.into(),
}
}
};
}
/// The default space name: the node's `name` field, else `<anonymous>`.
///
/// A free function as well as the trait default so a language that needs
/// to name a *few* kinds specially can delegate the rest rather than
/// restate the rule — `<get>` / `<set>` / `<init>` / `<static-init>` in
/// Kotlin, Java and Groovy all do (#1184). Calling `Self::…` there would
/// recurse.
#[must_use]
pub fn default_func_space_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
_ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
// we're in a function or in a class
if let Some(name) = node.child_by_field_name("name") {
node_text(code, &name)
} else {
Some("<anonymous>")
}
}
/// Per-language accessors the space walker and the Halstead
/// operator/operand classification dispatch through.
///
/// # Precondition
///
/// Every method taking a `code: &[u8]` next to a `&Node` slices `code`
/// by that node's byte range. `code` must be the exact buffer `node` was
/// parsed from — `big_code_analysis::Ast::source` for a node obtained from the
/// same `big_code_analysis::Ast`. Pairing a node with any other buffer reads the
/// wrong bytes at best and panics on an out-of-bounds index at worst;
/// the same precondition is documented on [`crate::dump_node`] (#795).
/// `node_text` — reached from the default `get_func_space_name` and
/// from the per-language `get_func_name` overrides — is the sole
/// bounds-checked slice; see its docs for why the rest deliberately
/// are not.
#[doc(hidden)]
/// The kinds one Tcl-family dialect spells the braced-word construct
/// with, for [`Getter::is_subsumed_braced_word`] and
/// [`Getter::braced_word_op_type`]. A struct rather than a row of `u16`
/// parameters because the ids are same-typed and positional: a
/// transposed pair compiles and silently inverts the rule for that
/// dialect.
pub struct BracedWordKinds {
/// `braced_word_simple`, the literal *value* form.
pub value: u16,
/// `braced_word`, the *script* form.
pub script: u16,
/// `comment`, the one named child of a script that is not a
/// command.
pub comment: u16,
/// `command`, the generic command node. Its `name` field carries
/// the leading word [`Getter::is_value_braced_word`] recognises the
/// construct by, and it is the only parent a *generic* argument
/// list hangs from. The hidden `_command` supertype (`Command2` in
/// both enums) is deliberately absent: the parser never emits it
/// (grammar-dispatch §2).
pub command: u16,
/// `word_list`, a command's `arguments` field — and, in both
/// grammars, also the argument list of the modelled `namespace`
/// construct, which is why the rule reads the grandparent rather
/// than stopping here.
pub word_list: u16,
/// `simple_word`, the only spelling of a command name this rule
/// resolves. A computed name (`$cmd {…}`, `[pick] {…}`) parses as
/// `variable_substitution` / `command_substitution` and is not
/// statically resolvable at all.
pub simple_word: u16,
/// `argument`, one entry of a `proc` parameter list. A modelled
/// slot that holds a *value* rather than a script: a defaulted
/// parameter (`proc p {a {b {x y}}}`) spells its default as a
/// `braced_word`, and a default is never evaluated as code.
pub argument: u16,
/// `procedure`, the modelled `proc` construct. Its `name` field is a
/// `braced_word` when the name holds a space (`proc {my proc} …`),
/// and a name is a literal although the construct's other braced
/// slot, the body, is a script
/// ([`Getter::is_braced_literal_slot`]).
pub procedure: u16,
/// `namespace`, the modelled construct — not the keyword token that
/// shares its name. Its `word_list` holds a subcommand followed by
/// that subcommand's arguments, and only three subcommands take a
/// script ([`Getter::is_braced_literal_slot`]).
pub namespace: u16,
/// `{`, the brace opener — the *only* node
/// [`Getter::braced_word_op_type`] may revise. A braced word's
/// operator children are not the opener alone: `_terminator` is a
/// hidden rule, so a `;` separating two commands is inlined as a
/// direct child of the `braced_word` and both dialects classify it
/// as an operator. Keying the revision on the parent kind alone
/// therefore swallowed those separators too, taking
/// `lappend x {puts a ; puts b}` to `n1` 0 / `N1` 0 and
/// `halstead.effort` — a gated threshold metric — to `0.0`.
pub open_brace: u16,
}
/// Per-language accessors that *name* and *classify* what a node is:
/// the function or space name, the [`SpaceKind`] a node opens, and the
/// [`TokenRole`] of a leaf.
///
/// Every method has a default that answers "nothing" (`None`,
/// [`SpaceKind::Unknown`], [`TokenRole::Unknown`]); a language
/// overrides only the ones its grammar expresses.
pub trait Getter {
/// Names the function `node` declares. Defaults to
/// [`get_func_space_name`](Self::get_func_space_name).
#[must_use]
fn get_func_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
Self::get_func_space_name(node, code, ancestors)
}
/// Names the space `node` opens.
///
/// `ancestors` is the chain the caller descended through. Elixir
/// needs it: its `def` / `defmodule` heads are ordinary `Call`
/// nodes, and one inside a `quote` template names no space at all,
/// which is a question about what encloses the call (#1088).
#[must_use]
fn get_func_space_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
default_func_space_name(node, code, ancestors)
}
/// The kind of space `node` opens, or [`SpaceKind::Unknown`] when it
/// opens none.
#[must_use]
fn get_space_kind(_node: &Node) -> SpaceKind {
SpaceKind::Unknown
}
/// Source-aware variant of [`get_space_kind`](Self::get_space_kind). The default
/// forwards to the byte-less classifier; languages whose space
/// kinds are encoded in macro identifier text (Elixir's
/// `defmodule` / `def` / `defp` / `defmacro` / `defmacrop` Calls)
/// override this so the walker can attribute the correct
/// `SpaceKind` to each promoted func space (#275).
///
/// `ancestors` is the chain the caller descended through; Elixir
/// needs it to see whether the `Call` sits inside a `quote`
/// template without paying `Node::parent`'s `O(depth)` (#1084).
#[inline]
#[must_use]
fn get_space_kind_with_code<'a>(
node: &Node<'a>,
_code: &[u8],
_ancestors: Ancestors<'a, '_>,
) -> SpaceKind {
Self::get_space_kind(node)
}
/// Classifies `node` as a Halstead operator, operand, or neither.
///
/// **Punctuation and delimiters are vocabulary, so they classify as
/// operators whatever grammatical role they serve.** Halstead
/// counts the alphabet a program is written in and has no notion of
/// a decision, so a `<` is the same vocabulary entry whether it
/// opens a JSX tag, brackets a Lua `<const>` attribute, names a C#
/// `operator <` overload, disambiguates a Kotlin `super<A>`, or
/// delimits a Perl `<FH>` readline. Every impl that classifies
/// punctuation at all bills `(`, `{`, `[`, `,`, `;`, `.` and `:` on
/// exactly that reading, for each glyph its language spells. Python
/// and Rust were the last two exceptions, closed by #1486.
///
/// A *decision* metric gates the same token by role, and is
/// supposed to disagree: ABC's `conditions` counts a `<` only under
/// a `binary_expression` (#1274, #1275, #1280, #1297), so each of
/// the five constructs above scores zero there while `bca ops`
/// still reports the bracket. #1395 settled that the two answers
/// are both correct rather than a drift to be reconciled;
/// `tests/parity/abc_halstead_bracket_parity.rs` fails if a later
/// consistency pass flips either side.
///
/// The one suppression that reading admits is **a literal's own
/// delimiter**. There the punctuation is not vocabulary in its own
/// right — the literal it wraps is already counted, so billing its
/// quotes fabricates an arithmetic or comparison operator the
/// source never spelled, and the score moves with the author's
/// choice of delimiter. It takes two shapes, and only the first is
/// an arm of this method.
///
/// Nine arms suppress a delimiter whose enclosing literal node is
/// itself the operand, each guarded on the delimiter's *parent*
/// being that literal: Elixir's sigil (#1256), the regex `/` of the
/// JS family (#1314), of Perl and of Ruby (#1312), Ruby's subshell
/// backtick (#1360), Groovy's slashy string and the raw-string `(`
/// of both C++ grammars (#1314), and Objective-C's `@` before an
/// `NSString` literal.
///
/// The tenth is [`braced_word_op_type`], which Tcl and iRules route
/// their `{` through. A braced *value* is a literal to
/// [`Checker::is_string_with_code`] and to the dump, so its opener
/// is withdrawn for the same reason — `puts {c d}` and
/// `puts "c d"` have to agree on the operator column — but the
/// operands are the words *inside* it rather than the braced word
/// itself, so the parent test above does not describe it. #1318
/// established that arm and #1382 widened it to the value slots of
/// a script-taking command.
///
/// A bracket delimiting *syntax* fits neither shape: no literal
/// would absorb its contribution, so it stays an operator. A
/// generic list's `<` / `>` is billed like the other bracket pairs,
/// once as `<>` on the opener ([`Self::get_operator_spelling`], #1559),
/// which leaves the comparison `<` and `>` their own entries.
///
/// `ancestors` is the chain the walker descended through. Most
/// impls read a parent from it to disambiguate a token whose role
/// depends on what encloses it — the nine literal-delimiter arms
/// above, plus, among others, Python's `not` / `in` / `is` inside
/// the compound `not in` / `is not`, Rust's `||` and `!` inside a
/// binary expression rather than a doc-comment marker, the
/// namespace identifier in both C++ grammars, Bash's `$name`, the
/// name inside a Tcl or iRules variable substitution, and Groovy's
/// `super` in a wildcard bound. Reaching those parents with
/// [`Node::parent`] instead costs `O(depth)` per node (#1096).
///
/// [`braced_word_op_type`]: Self::braced_word_op_type
/// [`Checker::is_string_with_code`]: crate::Checker::is_string_with_code
#[must_use]
fn get_op_type<'a>(_node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> TokenRole {
TokenRole::Unknown
}
/// Source-aware variant of [`get_op_type`]. The default forwards
/// to the byte-less classifier; languages whose Halstead operand
/// classification depends on token text override this. Kotlin uses
/// it to recover the variable in a short-form string template
/// (`"Hi $name"`), which the grammar emits as bare `string_content`
/// tokens with no structured interpolation node — the distinction
/// between an interpolated `$name` and a literal `$5` is only
/// visible in the source bytes (#454).
///
/// [`get_op_type`]: Self::get_op_type
#[inline]
#[must_use]
fn get_op_type_with_code<'a>(
node: &Node<'a>,
_code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> TokenRole {
Self::get_op_type(node, ancestors)
}
/// The fixed spelling an operator `node` is keyed under in place of
/// its kind id, for a token whose kind the grammar shares with a
/// different operator; `None` keys it by kind. A generic list's `<`
/// is the case (#1559): it has the comparison's kind but is keyed as
/// the `"<>"` pair, as `()`, `[]` and `{}` are. Consulted only for a
/// node [`get_op_type_with_code`](Self::get_op_type_with_code) calls
/// an operator.
#[inline]
#[must_use]
fn get_operator_spelling<'a>(
_node: &Node<'a>,
_ancestors: Ancestors<'a, '_>,
) -> Option<&'static str> {
None
}
/// Returns the source-byte slice used to key a Halstead *operand*.
/// The default keys on the operand node's full byte range. Kotlin
/// overrides this to narrow a short-interpolation name token
/// (`$name`) to its leading identifier prefix, because the grammar
/// glues trailing inter-segment text onto the name token
/// (`"$a $b"` → `"a "`); keying the raw bytes would record a
/// distinct `"a "` operand and break parity with the long `${a}`
/// form (#454).
#[inline]
#[must_use]
fn get_operand_id<'a>(
node: &Node<'a>,
code: &'a [u8],
_ancestors: Ancestors<'a, '_>,
) -> &'a [u8] {
&code[node.start_byte()..node.end_byte()]
}
/// Classifies a string-literal `node` as a single Halstead
/// operand, *unless* it wraps an interpolation child drawn from
/// `interp_kinds` — in which case the wrapper yields
/// [`TokenRole::Unknown`] because the inner expressions are
/// walked and counted separately. Counting the wrapper too would
/// double-count their contribution to `N2`.
///
/// This declares the per-language interpolation skip once (issue
/// #420), replacing nine independently-added regression fixes
/// (#183 / #184 / #191 / #192 / #199 / #277, …). Each language
/// supplies only its own grammar's interpolation child-kind ids;
/// the per-call rationale lives at each call site.
#[must_use]
fn string_operand_type(node: &Node, interp_kinds: &[u16]) -> TokenRole {
if node.wraps_any(interp_kinds) {
TokenRole::Unknown
} else {
TokenRole::Operand
}
}
/// Whether `node` is a fragment of a Tcl-family braced word rather
/// than a token of its own, and so contributes no operand (#1354).
///
/// The two dialects are deliberate clones, so the rule is declared
/// once and instantiated with each grammar's [`BracedWordKinds`];
/// the per-call rationale lives at each call site. It answers yes
/// in two cases:
///
/// - `node` sits directly inside a braced *value*
/// (`braced_word_simple`). Tcl evaluates nothing between braces,
/// so the value is its whole span and the parts inside it are
/// letters — the wrapper carries the one operand. This subsumes
/// #1314's narrower guard on the opening `{`, which is one more
/// such child.
/// - `node` is a braced *script* (`braced_word`) holding at least
/// one command — a named child other than a comment. Commands are
/// what the walk descends into and counts in their own right, so
/// billing the block as well would count its whole text a second
/// time. A comment is named too, but no arm classifies it, so a
/// body holding only comments is billed like an empty one rather
/// than like nothing at all.
///
/// A `script` with no command is deliberately not subsumed: the
/// same kind serves as the value slot of every command the grammar
/// does not special-case, where `lappend l {}` is an empty list and
/// the brace pair is its only carrier (grammar-dispatch §6). An
/// empty or comment-only `proc` body is spelled identically and so
/// also scores one operand, its whole text. #1318 can now tell the
/// two apart by the enclosing command, but it bills them alike, so
/// this arm needs no help from it.
///
/// This is the *byte-less* half of the braced-word rule and it is
/// not the whole of it. [`braced_word_op_type`] suppresses the `{`
/// of a script-kind word the enclosing command shows to be a plain
/// value, and both Tcl-family getters reach it through
/// [`get_op_type_with_code`], which is what the walk calls
/// (grammar-dispatch §7). It revises no operand, so this arm's
/// answers stand unchanged.
///
/// [`braced_word_op_type`]: Self::braced_word_op_type
/// [`get_op_type_with_code`]: Self::get_op_type_with_code
#[must_use]
fn is_subsumed_braced_word<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
ancestors.parent_has_kind(node, kinds.value)
|| (node.kind_id() == kinds.script
&& node
.children()
.any(|child| child.is_named() && child.kind_id() != kinds.comment))
}
/// The generic `command` node `word` is an argument of, paired with
/// that command's own ancestry — or `None` when `word` instead
/// fills a slot of a construct the grammar models, and so is a
/// script by construction.
///
/// Both dialects hang a *generic* command's arguments off a
/// `word_list` and a *modelled* construct's slots off that
/// construct's own node (`procedure`, `if`, `else`, `elseif`,
/// `while`, `foreach`, `catch`, `try`, `finally`, `argument`, plus
/// iRules' `when_event`, `for`, `switch_arm`, `on_handler`,
/// `trap_handler` and the three `dict` loops). Asking the parent
/// kind therefore answers "is this a generic argument" without
/// enumerating the modelled parents — a list that would be a
/// coverage claim to re-derive on every grammar bump
/// (grammar-dispatch §1), and that a grammar gaining one more
/// modelled construct would silently invalidate.
///
/// A `word_list` whose own parent is *not* a `command` is
/// `namespace`'s argument list — the one modelled construct in
/// either grammar that reaches its body through a `word_list`. The
/// grandparent test answers `None` there, so `namespace eval ns
/// {…}` keeps its body a script.
///
/// A braced word directly under a `command` is the command *name*
/// (`{puts} hi`), not an argument, and also answers `None`: its
/// caller decides that case before asking, because a name is
/// always a literal and must not pick up the `switch`-arm rescue
/// below.
#[must_use]
fn generic_argument_command<'tree, 'chain>(
word: &Node<'tree>,
ancestors: Ancestors<'tree, 'chain>,
kinds: &BracedWordKinds,
) -> Option<(Node<'tree>, Ancestors<'tree, 'chain>)> {
let mut chain = ancestors.iter(word);
let (parent, _) = chain.next()?;
if parent.kind_id() != kinds.word_list {
return None;
}
let (grandparent, grandparent_ancestors) = chain.next()?;
(grandparent.kind_id() == kinds.command).then_some((grandparent, grandparent_ancestors))
}
/// Whether a braced *script*-kind word is really a plain value —
/// the literal `{a b}` of `lappend x {a b}` rather than the block
/// of `eval {…}` (#1318).
///
/// The two roles share one kind, so no kind-scoped arm can separate
/// them; recognition is out-of-band, by the enclosing command's
/// leading word (grammar-dispatch §9). A word that fills a modelled
/// construct's slot is a script; a word passed to a command in
/// `SCRIPT_TAKING_COMMANDS` is a script; **everything else is a
/// value**.
///
/// That default is the load-bearing choice, and it is deliberately
/// the opposite of "keep today's answer for anything unrecognised":
///
/// - The script-taking set is *closed* and small — Tcl defines no
/// user-extensible control structures, so a command that
/// evaluates a braced argument is either a core command listed
/// above or a proc that forwards to `eval` / `uplevel`. The
/// value-taking set is *open*: every user proc and every package
/// command that takes a list or a pattern is in it. An
/// unrecognised name is therefore far likelier to be value-taking.
/// - Defaulting to script *fabricates* — it reports a `{}` operator
/// for a block the source does not contain. Defaulting to value
/// can only *omit*: one `N1` occurrence per script handed to an
/// unlisted command, and — when no modelled construct in the same
/// space opens a block — the `{}` vocabulary entry with them.
/// That last case is where the cost shows: a top-level
/// `dict for {k v} $d { puts $k }` with nothing around it has no
/// operator left, and Halstead's difficulty is a product with
/// `n1` in it, so `effort` reads `0.0` rather than a little low.
/// Inside a `proc` the `proc` keyword and body brace keep the
/// space non-zero, which is the scope `halstead.effort` is gated
/// at; and the same `0.0` was already the answer for a file of
/// `set x {a b}` lines, so it is Halstead's shape on an
/// operator-free space, not a new failure mode. The omission
/// stays bounded only because the answer is scoped to the brace:
/// see [`braced_word_op_type`] for the measurement that decided
/// it, and for why suppressing the *contents* of a value would
/// have made it unbounded instead.
/// - It stops the score moving with the author's choice of
/// delimiter, which is what #695, #1312 and #1314 each restored
/// elsewhere: `puts {c d}` and `puts "c d"` now agree on the
/// operator column.
///
/// The cost is a script passed to an unlisted command — a Tk
/// `-command {…}` callback, `trace add variable v w {…}`, a
/// subcommand dispatch (`dict for`, `interp eval`), a user-defined
/// `with_lock {…}` — losing the `{}` its block deserves. No
/// structural signal distinguishes those cases: the grammar parses
/// `{a b}` and `{puts hi}` into the same shape. The code inside
/// them is still counted, so the loss is one operator occurrence
/// per such block, plus the vocabulary entry in the case above.
///
/// [`braced_word_op_type`]: Self::braced_word_op_type
#[must_use]
fn is_value_braced_word<'a>(
word: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
// Two positions answer "value" without consulting any command
// name, and both must be asked before the lookup below,
// because neither hangs off a `command` the way an argument
// does — and the second must not reach the `switch`-arm
// rescue.
//
// A defaulted `proc` parameter (`proc p {a {b {x y}}}`) is the
// one modelled slot holding a value: a default is data the
// interpreter assigns, never a script it evaluates.
//
// A braced word directly under a `command` is that command's
// *name* (`{puts} hi` invokes `puts`), so it is a literal word.
// Inside a `switch` arm list the name is the arm's *pattern*,
// and a braced pattern (`switch -regexp $v { {^a.*b$} {…} }`)
// is idiomatic — rescuing it as an arm body would fabricate a
// block around a regex.
if ancestors
.parent(word)
.is_some_and(|parent| [kinds.argument, kinds.command].contains(&parent.kind_id()))
{
return true;
}
let Some((command, command_ancestors)) =
Self::generic_argument_command(word, ancestors, kinds)
else {
return false;
};
if Self::command_leading_word(&command, code, kinds)
.and_then(script_slots)
.is_some()
{
return false;
}
!(Self::is_switch_arm(&command, code, command_ancestors, kinds)
&& Self::is_switch_arm_body(word, &command))
}
/// Whether `node` is a Tcl-family braced *script* — a `proc` or `if`
/// body, an iRules `when` handler — rather than a braced literal
/// (#1381).
///
/// The positive form of [`is_value_braced_word`], narrowed to the
/// script kind so it answers `false` for every node of every other
/// kind and for the two literal spellings, and narrowed again by the
/// value slots [`is_braced_literal_slot`] recognises. That makes it
/// the question the *non-Halstead* classifiers ask:
/// `Checker::is_string_with_code` must not call a `proc` body a
/// string literal, and `Alterator::keeps_children` must stop the dump
/// flattening one into a leaf, which dropped the body from it. Both
/// once listed `braced_word` beside `quoted_word` and
/// `braced_word_simple`, which is right for the literal role and
/// wrong for the script role the same kind also serves.
///
/// Stated here rather than in each of the five call sites so the
/// string, dump and Halstead classifiers cannot drift apart on the
/// same bytes (grammar-dispatch §7). Halstead was the deliberate
/// exception between #1318 and #1382: `braced_word_op_type` asked
/// [`is_value_braced_word`] alone, so the braces of the value slots
/// [`is_braced_literal_slot`] adds still billed a `{}` operator
/// while the other two called the same bytes a literal. #1382
/// closed it — that call site now asks this predicate — so a braced
/// value is a value to all three, and `after {100} {puts hi}` bills
/// the one `{}` its `{puts hi}` block earns rather than two.
///
/// The remaining §7 note is one of scope, not of disagreement: the
/// three differ only where Halstead's *operand* half keys on
/// whether a body holds a command, which makes an empty or
/// comment-only `proc` body an operand there and not a string here.
///
/// [`is_value_braced_word`]: Self::is_value_braced_word
/// [`is_braced_literal_slot`]: Self::is_braced_literal_slot
#[must_use]
fn is_braced_script_word<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
node.kind_id() == kinds.script
&& !Self::is_value_braced_word(node, code, ancestors, kinds)
&& !Self::is_braced_literal_slot(node, code, ancestors, kinds)
}
/// Whether `word`, a braced word [`is_value_braced_word`] calls a
/// script, fills a slot whose documented syntax takes a *value*.
///
/// [`is_value_braced_word`] answers by command name alone. Most of the
/// names it recognises have a mixed signature, though, so that answer
/// made a *value* — a millisecond count, a namespace name, a `switch`
/// subject — a script (#1381 review). This
/// is the per-argument half: `proc` has one value slot the grammar
/// names as a field, and every other construct declares its layout in
/// one of the two tables in `lang_helpers::tcl_family`, which
/// `fills_script_slot` applies.
///
/// Each value here was a string and a flat dump leaf before #1381, and
/// would otherwise have become a script under it: the dump rendered
/// `{my proc}` as a command named `my` and `{100}` as one named `100`,
/// and `namespace export {…}` and `namespace ensemble create -map {…}`
/// both occur in the Tcl 8.6 standard library. Since #1382 Halstead
/// reads the same answer through [`is_braced_script_word`], so the
/// braces of these values quote rather than open (`after {100} {…}`
/// scores the `{}` of its script and no other).
///
/// Two guards keep the construct-wide answer. A switch arm list parses
/// as commands, so an arm whose *pattern* is spelled `proc`,
/// `namespace`, `after` or `switch` builds one of these shapes around
/// what are really arm bodies — [`is_switch_arm`] recognises it first.
/// And an argument list whose own slot sequence is unreadable has no
/// positions worth trusting.
///
/// That second guard is `tcl_family::argument_slots_are_readable`,
/// which asks the *slots* rather than the owner — asking
/// [`Node::has_error`] of the command withdrew every value slot of a
/// command whose only error sat inside one of its script arguments.
/// Its doc carries the measurement. A list *longer* than the
/// signature admits is the second way to be unreadable, and
/// `tcl_family::ScriptSlots::LastOf` is where that one lives.
///
/// A `try … trap` clause is out of reach only where the grammar
/// wraps it: inside a `proc` or a `namespace eval` body the Tcl
/// grammar recovers into an `ERROR` node and no role signal
/// survives, but at the top level the same clause parses as a plain
/// `trap` command whose slots this does read. An earlier revision
/// of this paragraph said "the multi-line `try … trap` clause" was
/// out of reach outright, which is where the `finally` defect
/// `LastOf` now guards hid (#1382 review).
///
/// [`Node::has_error`]: crate::Node::has_error
/// [`is_braced_script_word`]: Self::is_braced_script_word
/// [`is_value_braced_word`]: Self::is_value_braced_word
/// [`is_switch_arm`]: Self::is_switch_arm
#[must_use]
fn is_braced_literal_slot<'a>(
word: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
let mut chain = ancestors.iter(word);
// A value slot is an *argument*, so it sits at least two levels
// below the root; neither grammar puts a braced word at the root
// or directly under it (a statement-position braced word is
// wrapped in a `command`, so `{a b}` alone on a line is still two
// deep). Both exits are therefore unreachable from a walk and are
// here because this is a `pub` trait method any node may be
// handed — `braced_slot_tests` calls it at both depths.
let Some((parent, above_parent)) = chain.next() else {
return false;
};
if parent.kind_id() == kinds.procedure {
let is_name =
matches!(parent.child_by_field_name("name"), Some(name) if name.id() == word.id());
return is_name && !Self::is_switch_arm(&parent, code, above_parent, kinds);
}
let Some((owner, above_owner)) = chain.next() else {
return false;
};
if parent.kind_id() != kinds.word_list
|| !argument_slots_are_readable(&owner, &parent)
|| Self::is_switch_arm(&owner, code, above_owner, kinds)
{
return false;
}
let dialect = Dialect { code, kinds };
// At both pinned grammars a `word_list` has exactly two possible
// owners — `command`'s `arguments` field and `namespace`'s child
// — so the third arm is unreachable and stays as the answer a
// grammar that grew a third owner should get: no layout, hence
// the construct-wide script answer.
// `a_word_list_is_owned_only_by_a_command_or_a_namespace` fails
// when that stops being true, which is the event this arm exists
// for.
let roles = if owner.kind_id() == kinds.namespace {
namespace_script_slots(&parent, dialect).map(|slots| ArgumentRoles { slots, first: 1 })
} else if owner.kind_id() == kinds.command {
Self::command_leading_word(&owner, code, kinds)
.and_then(script_slots)
.map(|slots| ArgumentRoles { slots, first: 0 })
} else {
None
};
roles.is_some_and(|roles| !fills_script_slot(word, &parent, roles, dialect))
}
/// Whether `word`, an argument of a `switch` arm command
/// ([`is_switch_arm`]), sits in a *body* position rather than a
/// *pattern* position.
///
/// The arm list is a flat run of `pattern body pattern body …`,
/// and the grammar breaks it into commands at newlines. One arm per
/// line gives each command a pattern for its name and a body for
/// its sole argument; several arms on one line give the first
/// pattern's command the whole run as arguments, so the arguments
/// alternate `body pattern body …`. The command-name test above
/// keeps the *first* braced pattern a literal, but without this
/// gate every later one was rescued as a body, and a one-line
/// `switch -regexp $v { {^a} {…} {^b} {…} }` fabricated a `{}`
/// around `{^b}` that the same arms written one per line did not —
/// the score moving with layout, which is what the rule exists to
/// stop.
///
/// Position is the only signal because it is what Tcl itself uses:
/// `switch` pairs the list up by index, with no marker on either
/// half. The parity holds across the words that can interpose — a
/// `-` fall-through body and a `default` pattern are both
/// `simple_word`s and take a slot each — so an even index is a body
/// and an odd one a pattern. This is the one place the rule needs a
/// sibling *index* rather than a parent kind, and it runs once per
/// braced argument of such a command.
///
/// The index comes from [`Cursor::goto_first_child_for_byte`], which
/// is `O(log n)` in the argument count. A `children().position(..)`
/// scan answers identically in `O(n)`, and since every braced word
/// of a one-line arm list asks, that made the Halstead walk — and,
/// once #1381 routed them through this rule, `find` / `count
/// --type string` and the `Ast` dump — quadratic in the width of
/// one line: seconds per request at tens of KB (#1381 review). The
/// id check keeps the scan's answer for a word that is not one of
/// the command's arguments at all.
///
/// [`is_switch_arm`]: Self::is_switch_arm
/// [`Cursor::goto_first_child_for_byte`]: crate::node::Cursor::goto_first_child_for_byte
#[must_use]
fn is_switch_arm_body(word: &Node<'_>, command: &Node<'_>) -> bool {
// `is_value_braced_word` asks only for a word it has already
// placed inside this command's `arguments`, so the field is
// always there on that path. It is not always there on the
// method's own contract — an argument-less `switch` is a
// `command` with a `name` and nothing else — and such a command
// has no body position at all.
let Some(arguments) = command.child_by_field_name("arguments") else {
return false;
};
let mut cursor = arguments.cursor();
let index = cursor.goto_first_child_for_byte(word.start_byte());
cursor.node().id() == word.id() && matches!(index, Some(index) if index.is_multiple_of(2))
}
/// A command's leading word, when it is a statically resolvable
/// `simple_word`.
///
/// Located by field rather than by index (grammar-dispatch §3): the
/// name is `command`'s `name` field in both grammars. A computed
/// name (`$cmd {…}`, `[pick] {…}`) parses as a substitution node and
/// is not resolvable at all, and a name spelled in quotes or braces
/// (`"eval" {…}`) is legal Tcl that this deliberately leaves
/// unresolved — the same limitation `tcl_command_name` records for
/// the Cognitive and Cyclomatic walkers.
#[must_use]
fn command_leading_word<'c>(
command: &Node<'_>,
code: &'c [u8],
kinds: &BracedWordKinds,
) -> Option<&'c str> {
let name = command.child_by_field_name("name")?;
if name.kind_id() != kinds.simple_word {
return None;
}
let text = node_text(code, &name)?;
// `::eval` *is* `eval`, and without the strip the qualified form
// fell to the value default and lost its block. The rule and the
// reason it stops at the *leading* qualifier live with the
// helper, which the four metrics that resolve a leading word
// without this table share (#1381 review).
Some(crate::lang_helpers::strip_global_qualifier(text))
}
/// Whether `command` is really one `pattern body` pair of a Tcl
/// `switch` arm list rather than a command of its own.
///
/// Tcl models no `switch`, so `switch $v {a {…} b {…}}` parses the
/// arm list as a braced word whose interior is a `command` named
/// after the *first pattern* — `a` here — with the arm bodies as
/// its arguments (grammar-dispatch §9; the Cognitive walker reads
/// the same shape through `tcl_switch_arm_list`). Without this
/// test the bodies would read as literals passed to a command
/// called `a`, which is exactly what the value default is for
/// everywhere else. iRules models `switch` with `switch_arm`
/// children, so its arm bodies never reach here.
#[must_use]
fn is_switch_arm<'a>(
command: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> bool {
ancestors
.iter(command)
.next()
.is_some_and(|(arm_list, above)| {
arm_list.kind_id() == kinds.script
&& Self::generic_argument_command(&arm_list, above, kinds)
.and_then(|(switch, _)| Self::command_leading_word(&switch, code, kinds))
== Some(SWITCH_COMMAND)
})
}
/// Whether the `{` of `node`'s parent braced word opens a block,
/// or merely quotes a literal (#1318).
///
/// This is #1314's guard with its kind test replaced by a role
/// test. #1314 suppressed the opener of a `braced_word_simple`,
/// the literal form the grammars emit only in the value slots they
/// special-case; everywhere else a literal is a `braced_word`, the
/// same kind a block uses, and its `{` still reported a `{}`
/// operator for a block the source does not contain —
/// `lappend x {a b}`, `puts {c d}`, and every user proc taking a
/// list.
///
/// The role is [`is_braced_script_word`], the same predicate
/// `Checker::is_string_with_code` and `Alterator::keeps_children`
/// ask, so the three classifiers answer one question about one set
/// of bytes (grammar-dispatch §7). That was not so between #1318
/// and #1382: this call site asked [`is_value_braced_word`] alone
/// and billed a `{}` for the value slots of a script-taking
/// command, which the other two had called literals since #1381.
/// The exception, and what closing it moved, is recorded on
/// [`is_braced_script_word`], which owns the §7 story for all five
/// call sites.
///
/// **It revises the operator only, and deliberately leaves the
/// words inside a value alone.** Suppressing them too would make
/// `lappend x {a b}` score like its `set x {a b}` synonym, which
/// is the tidier answer for that line — and the wrong one in
/// general, because the same braced argument of an unrecognised
/// command is just as often real code: an `oo::class create C {…}`
/// body, a `tcltest` `-body {…}`, an `apply {{x} {…}}` lambda.
/// Suppressing contents was built and measured on a file holding
/// one of each; it took n1 5 → 2, N1 8 → 2, n2 25 → 15 and
/// N2 32 → 15, the class body, the test body and the lambda each
/// collapsing into a single operand, and `halstead.effort` — a
/// gated threshold metric — collapsing with them. So the rule only
/// ever withdraws a claim the classifier cannot support; it never
/// discards code the walk has already read.
///
/// The residual is the asymmetry #1382 opens with, and it is the
/// **contract**, not an outstanding defect: a braced value scores
/// one operand where the grammar names the command (`set x {a b}`)
/// and one per word where only the command name would
/// (`lappend x {a b}`). #1382 weighed the two ways of closing it
/// and took neither. Reading the *contents* — no substitution, no
/// nested block, therefore list-like — is the measurement above,
/// already taken and already rejected. Widening the one-operand
/// treatment to a *value*-taking command list needs the mirror of
/// `SCRIPT_TAKING_COMMANDS`, and that set is open where this one is
/// closed: every user proc is in it, so the list is incomplete by
/// construction and would leave three behaviours where there are
/// two. Both trade a bounded over-report of the operand vocabulary
/// for an unbounded loss of real code, which is the trade the
/// paragraph above declines.
///
/// Keeping to the operator also keeps the whole thing cheap: only
/// a braced word's own opener can change answer, so the test is one
/// kind comparison and one parent lookup, the same scope #1354 and
/// #1314 use. Two lookups are `O(log n)` rather than `O(1)`, both
/// cursor seeks by byte and both reached only from a *command whose
/// name resolves*: [`is_switch_arm_body`] needs a `switch` arm
/// word's index among its command's arguments, and — since #1382 —
/// [`is_braced_literal_slot`] needs the same index to place a word
/// against the layout `lang_helpers::tcl_family` records. An
/// ancestor scan would have been `O(depth)` per node and quadratic
/// on a deeply nested `expr`, the shape #1122 warns about.
///
/// [`is_braced_literal_slot`]: Self::is_braced_literal_slot
/// [`is_braced_script_word`]: Self::is_braced_script_word
/// [`is_switch_arm_body`]: Self::is_switch_arm_body
/// [`is_value_braced_word`]: Self::is_value_braced_word
///
/// [`get_op_type`]: Self::get_op_type
#[must_use]
fn braced_word_op_type<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
kinds: &BracedWordKinds,
) -> TokenRole {
let base = Self::get_op_type(node, ancestors);
// Only the opener can change answer, and the test has to say so
// rather than infer it from the parent kind. A braced word's
// operator children are the `{` *and* every `;` separating two
// of its commands — `_terminator` is a hidden rule, so the
// separator is inlined as a direct child of the `braced_word`
// and both dialects list `SEMI` as an operator. The closer is
// unclassified and a `\n` terminator is not an operator, so
// those two are the whole set.
if node.kind_id() != kinds.open_brace || !matches!(base, TokenRole::Operator) {
return base;
}
let quotes_a_literal = ancestors.iter(node).next().is_some_and(|(parent, above)| {
parent.kind_id() == kinds.script
&& !Self::is_braced_script_word(&parent, code, above, kinds)
});
if quotes_a_literal {
TokenRole::Unknown
} else {
base
}
}
/// The grammar's name for the operator kind `id`, used to render a
/// Halstead operator; empty for languages that do not report them.
#[must_use]
fn get_operator_id_as_str(_id: u16) -> &'static str {
""
}
}
mod bash;
mod c;
mod ccomment;
mod cpp;
mod csharp;
mod elixir;
mod generic_angles;
mod go;
mod groovy;
mod irules;
mod java;
mod javascript;
mod js_family;
mod kotlin;
mod lua;
mod mozcpp;
mod mozjs;
mod objc;
mod perl;
mod php;
mod preproc;
mod python;
mod ruby;
mod rust;
mod tcl;
mod tsx;
mod typescript;
#[cfg(test)]
mod node_text_tests {
use super::node_text;
use crate::langs::RustParser;
use crate::traits::ParserTrait;
use std::path::PathBuf;
/// A node whose span lies inside the buffer it was parsed from
/// yields its exact source text.
#[cfg(feature = "rust")]
#[test]
fn in_bounds_span_returns_text() {
let src = "fn x() {}";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
let root = parser.root();
assert_eq!(node_text(parser.code(), &root), Some(src));
}
/// Reslicing a node against a *shorter* buffer (the stale-span hazard
/// the guard exists for) must degrade to `None`, not panic. A direct
/// `&code[start..end]` would panic here — this is the revert check.
#[cfg(feature = "rust")]
#[test]
fn out_of_bounds_span_returns_none_not_panic() {
let src = "fn x() {}";
let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
let root = parser.root();
assert!(root.end_byte() > 2);
let truncated = &src.as_bytes()[..2];
assert_eq!(node_text(truncated, &root), None);
}
/// The UTF-8 guard is the *other* failure mode, and unlike the range
/// guard it is reachable without violating the same-parse
/// precondition: `Ast::parse` accepts arbitrary bytes. A span whose
/// bytes are not valid UTF-8 must yield `None`, not a panic and not
/// lossy replacement characters.
#[cfg(feature = "rust")]
#[test]
fn non_utf8_span_returns_none() {
let mut src = b"fn ".to_vec();
src.extend_from_slice(&[0xF0, 0x9F]);
src.extend_from_slice(b"() {}");
let parser = RustParser::new(src.clone(), &PathBuf::from("t.rs"), None);
let root = parser.root();
assert_eq!(root.end_byte(), src.len());
assert_eq!(node_text(parser.code(), &root), None);
}
}
#[cfg(test)]
mod ancestor_tests {
use super::Getter;
use crate::node::{Ancestors, Node};
use crate::test_support::for_each_node_with_chain;
use crate::traits::LanguageInfo;
/// `get_func_space_name` must name a space the same whether it reads
/// the walker's ancestor chain or climbs with `Node::parent`.
///
/// Two grammars consult an ancestor here, for different reasons:
/// the JS family names an anonymous `function` / arrow from the
/// `pair` or `variable_declarator` holding it, and Elixir skips
/// naming a `def` that sits inside a `quote` template. #1088 moved
/// both onto the chain.
#[cfg(any(
feature = "elixir",
feature = "javascript",
feature = "mozjs",
feature = "typescript",
))]
fn assert_name_parity<L: LanguageInfo + Getter>(
label: &str,
code: &[u8],
expect_named: &[&str],
) {
let mut seen: Vec<&str> = Vec::new();
let visited = for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
let known = L::get_func_space_name(node, code, Ancestors::known(chain));
let climbing = L::get_func_space_name(node, code, Ancestors::unknown());
assert_eq!(
known,
climbing,
"{label}: name of {} at row {} disagrees",
node.kind(),
node.start_row()
);
if let Some(name) = known
&& expect_named.contains(&name)
&& !seen.contains(&name)
{
seen.push(name);
}
});
assert!(visited > 20, "{label}: fixture is too small to prove much");
for name in expect_named {
assert!(
seen.contains(name),
"{label}: no node resolved to {name:?}, so the fixture no longer \
exercises the ancestor-derived naming it was added for"
);
}
}
#[cfg(all(
feature = "elixir",
feature = "javascript",
feature = "mozjs",
feature = "typescript",
))]
#[test]
fn func_space_name_agrees_between_known_and_climbing() {
// `outer` and `keyed` are only reachable through the parent:
// the function expressions themselves carry no `name` field.
//
// All four JS-family grammars are exercised, not just
// JavaScript: their `get_func_space_name` impls are separate
// copies of the same body against four distinct `kind_id`
// enums, so a `Pair` / `VariableDeclarator` id that drifted in
// one grammar would be invisible here if only one were checked.
let js_source =
b"var outer = function () { return 1; };\nvar o = { keyed: function () { return 2; } };\n";
assert_name_parity::<crate::langs::JavascriptCode>(
"javascript",
js_source,
&["outer", "keyed"],
);
assert_name_parity::<crate::langs::MozjsCode>("mozjs", js_source, &["outer", "keyed"]);
assert_name_parity::<crate::langs::TypescriptCode>(
"typescript",
js_source,
&["outer", "keyed"],
);
assert_name_parity::<crate::langs::TsxCode>("tsx", js_source, &["outer", "keyed"]);
// `multi` is named from its `Call` head; the `def a` inside the
// `quote` template is not a definition, so it falls through to
// the field-less default.
assert_name_parity::<crate::langs::ElixirCode>(
"elixir",
b"defmodule Foo do\n defmacro multi do\n quote do\n def a, do: 1\n end\n end\nend\n",
&["Foo", "multi"],
);
}
}
/// The `is_braced_literal_slot` / `is_switch_arm_body` guards that no
/// Tcl-family walk can reach, and the grammar shape each rests on.
#[cfg(test)]
#[cfg(any(feature = "tcl", feature = "irules"))]
mod braced_slot_tests {
use super::{BracedWordKinds, Getter};
// Gated with the rows that name it: `Tcl` is read only by the
// `feature = "tcl"` helpers below, so the module's wider
// `any(tcl, irules)` gate would leave this unused — and `ci.yml` sets
// `RUSTFLAGS: "-D warnings"`, which turns that into a hard failure on
// an `irules`-without-`tcl` build.
#[cfg(feature = "tcl")]
use crate::Tcl;
use crate::node::{Ancestors, Node};
use crate::test_support::for_each_node_with_chain;
use crate::traits::LanguageInfo;
/// No node shallower than an argument can fill a value slot.
///
/// A value slot is an argument, and both grammars hang an argument at
/// least two levels below the root — under a `word_list` under a
/// `command`, or under a construct's own node. A statement-position
/// braced word is no exception: `{a b}` alone on a line is a `command`
/// wrapping the braced word, never the braced word itself. So the root
/// and its direct children are exactly the depths
/// `is_braced_literal_slot`'s two chain exits answer for, and no walk
/// reaches either.
///
/// They are still the method's contract — it is `pub`, takes any node,
/// and must not read a slot into something with no argument list above
/// it. Flipping either exit to `true` makes this fail.
fn assert_no_shallow_value_slot<L: LanguageInfo + Getter>(
label: &str,
code: &[u8],
kinds: &BracedWordKinds,
) {
let (mut roots, mut children) = (0, 0);
for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
if chain.len() > 1 {
return;
}
for ancestors in [Ancestors::known(chain), Ancestors::unknown()] {
assert!(
!L::is_braced_literal_slot(node, code, ancestors, kinds),
"{label}: a {} at depth {} filled a value slot",
node.kind(),
chain.len()
);
}
if chain.is_empty() {
roots += 1;
} else {
children += 1;
}
});
assert_eq!(roots, 1, "{label}: exactly one root");
assert!(
children > 1,
"{label}: the fixture must put several nodes directly under the \
root, or the depth-1 exit goes unexercised"
);
}
#[test]
#[cfg(feature = "tcl")]
fn tcl_no_node_above_an_argument_fills_a_value_slot() {
assert_no_shallow_value_slot::<crate::langs::TclCode>(
"tcl",
b"{a b}\nproc {my proc} {} {}\nnamespace export {c d}\n",
&crate::lang_helpers::tcl::BRACED_WORD_KINDS,
);
}
#[test]
#[cfg(feature = "irules")]
fn irules_no_node_above_an_argument_fills_a_value_slot() {
assert_no_shallow_value_slot::<crate::langs::IrulesCode>(
"irules",
b"{a b}\nproc {my proc} {} {}\nnamespace export {c d}\n",
&crate::lang_helpers::irules::BRACED_WORD_KINDS,
);
}
/// Every braced word of `code` that fills a value slot, in source
/// order. Tcl only: `is_braced_literal_slot` is a provided method, so
/// both dialects run the identical body over their own kind table.
///
/// Walks with [`Ancestors::unknown`] rather than through
/// `for_each_node_with_chain`, whose fixtures must parse cleanly so a
/// walk cannot cover error recovery by accident — which is exactly
/// what the callers below are about. The two spellings answer
/// identically here; `assert_no_shallow_value_slot` above asserts
/// that on every node it visits.
#[cfg(feature = "tcl")]
fn value_slot_texts(code: &[u8]) -> Vec<String> {
let tree = crate::node::Tree::new::<crate::langs::TclCode>(code);
tree.get_root()
.preorder()
.filter(|node| {
<crate::langs::TclCode as Getter>::is_braced_literal_slot(
node,
code,
Ancestors::unknown(),
&crate::lang_helpers::tcl::BRACED_WORD_KINDS,
)
})
.map(|node| node.utf8_text(code).unwrap_or_default().to_owned())
.collect()
}
/// A parse error nested inside one argument's *script* leaves its
/// sibling *value* slots alone (#1381 review).
///
/// `Node::has_error` is transitive, so asking it of the command
/// withdrew the rescue from every argument: `trap {pat} {v} {puts ]}`
/// lost both value slots, and the dump then rendered each as a
/// fabricated `command` named after its own text. What decides
/// whether the positions are readable is the argument list's own slot
/// sequence, and an error *inside* an argument moves no sibling's
/// index. Verified by reverting the guard to `owner.has_error()`:
/// the error row then comes back empty and this fails.
///
/// Asserted as the *same* list for both fixtures rather than as a
/// non-empty one for the error row. The well-formed row is what pins
/// that the fixture still spells two value slots at all, so trimming
/// one out fails here instead of quietly reducing the claim to
/// "nothing changed".
#[test]
#[cfg(feature = "tcl")]
fn an_error_inside_a_script_argument_keeps_the_sibling_value_slots() {
let well_formed = value_slot_texts(b"trap {pat} {v} {puts j}\n");
assert_eq!(
well_formed,
["{pat}", "{v}"],
"the fixture must spell two value slots before an error is added"
);
assert_eq!(
value_slot_texts(b"trap {pat} {v} {puts ]}\n"),
well_formed,
"an error inside the script argument moves no sibling's index"
);
}
/// The other half of the same rule: an `ERROR` token occupying a slot
/// of the argument list *does* shift every argument after it, so the
/// construct-wide answer is the right one there.
///
/// `on code varList script` reads its slots by position, and the
/// stray `]` takes one of its own — so `{code}` is no longer
/// followed by the variable list the signature puts there and the
/// layout cannot be trusted. Without this row the guard could be
/// deleted outright and the test above would still pass.
///
/// The fixture keeps the clause's three arguments deliberately: the
/// `]` replaces the variable list rather than joining it, so
/// `ScriptSlots::LastOf`'s arity rule still admits the list and
/// `argument_slots_are_readable` is the only guard that can reject
/// it. A four-argument spelling would be rejected twice over and
/// this row would stop naming which guard it tests (#1382 review).
///
/// The well-formed row is what keeps the empty one from passing
/// vacuously: `on` reaching the slot table at all is what makes the
/// error row's emptiness mean something, and dropping that row from
/// `SCRIPT_TAKING_COMMANDS` empties both. The siblings either side
/// of this test anchor the same way.
#[test]
#[cfg(feature = "tcl")]
fn an_error_occupying_a_slot_withdraws_the_layout() {
assert_eq!(
value_slot_texts(b"on {code} {v} {puts j}\n"),
["{code}", "{v}"],
"the fixture must spell two value slots before the error \
takes one of their places"
);
assert!(
value_slot_texts(b"on {code} ] {puts j}\n").is_empty(),
"an ERROR token in the argument list makes every position \
unreadable"
);
}
/// The third way a layout stops being readable: the grammar groups
/// a *following* clause into this one, so the list is longer than
/// the signature admits (#1382 review).
///
/// Tcl models neither `on` nor `trap` outside a `try`, so a
/// top-level `try … trap … finally` parses the whole tail as one
/// generic `trap` command — `p v b finally c`. Reading "the script
/// is the last argument" off that names the `finally` body and
/// calls the handler body a value, which withdraws the `{}` of a
/// real block and flattens it out of the dump. Both rows run the
/// same clause, so the second cannot pass by the fixture having
/// stopped spelling a value slot at all.
#[test]
#[cfg(feature = "tcl")]
fn a_handler_clause_of_the_wrong_arity_withdraws_the_layout() {
assert_eq!(
value_slot_texts(b"try {puts a} trap {p} {v} {puts b}\n"),
["{p}", "{v}"],
"a three-argument `trap` clause spells two value slots"
);
assert!(
value_slot_texts(b"try {puts a} trap {p} {v} {puts b} finally {puts c}\n").is_empty(),
"a trailing `finally` joins the same generic `trap` command, \
so no position in its argument list is trustworthy"
);
}
/// A command with no argument list has no `switch`-arm body position.
///
/// `is_value_braced_word` only ever asks about a word it has already
/// placed inside the command's `arguments`, so that field is always
/// present on the walk's path. It is absent for an argument-less
/// command — `switch` on a line by itself is a `command` carrying only
/// its `name` — and the method has to answer for one, because a
/// bodiless command has no even-indexed slot to be in.
///
/// The second half is the same method's positive answer on a real arm
/// list, so the first cannot be satisfied by a constant.
#[test]
#[cfg(feature = "tcl")]
fn a_command_without_arguments_has_no_switch_arm_body() {
use crate::node::Tree;
let code = b"switch\nswitch $v { p {puts a} }\n";
let tree = Tree::new::<crate::langs::TclCode>(code);
let root = tree.get_root();
let bare = root.child(0).expect("the bare `switch` command");
let name = bare
.child_by_field_name("name")
.expect("even a bare command names itself");
assert!(
bare.child_by_field_name("arguments").is_none(),
"the fixture's first command must carry no argument list"
);
assert!(
!<crate::langs::TclCode as Getter>::is_switch_arm_body(&name, &bare),
"a command with no argument list has no body position"
);
let arm = find_arm_named_p(&root, code).expect("the arm command named `p`");
let body = arm
.child_by_field_name("arguments")
.and_then(|args| args.child(0))
.expect("the arm's body");
assert!(
<crate::langs::TclCode as Getter>::is_switch_arm_body(&body, &arm),
"the arm's sole argument is its body, or the assertion above is \
satisfied by a constant"
);
}
/// The `command` a Tcl `switch` arm list spells its first pattern as.
#[cfg(feature = "tcl")]
fn find_arm_named_p<'a>(node: &Node<'a>, code: &[u8]) -> Option<Node<'a>> {
let named_p = node.kind_id() == Tcl::Command as u16
&& node
.child_by_field_name("name")
.is_some_and(|name| name.utf8_text(code) == Some("p"));
if named_p {
return Some(*node);
}
node.children()
.find_map(|child| find_arm_named_p(&child, code))
}
/// Only a `command` and a `namespace` own a `word_list`, which is what
/// makes `is_braced_literal_slot`'s third owner arm unreachable.
///
/// Checked against both pinned grammars' `node-types.json` when that
/// arm was written; this is the executable half, so a grammar bump
/// handing a `word_list` to a third construct fails here rather than
/// silently routing it to the no-layout answer.
fn assert_word_list_owners<L: LanguageInfo>(label: &str, code: &[u8], kinds: &BracedWordKinds) {
let mut seen = 0;
for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
if node.kind_id() != kinds.word_list {
return;
}
let owner = chain.last().expect("a word_list is never the root");
assert!(
[kinds.command, kinds.namespace].contains(&owner.kind_id()),
"{label}: a word_list owned by {}",
owner.kind()
);
seen += 1;
});
assert!(seen > 2, "{label}: fixture reached only {seen} word_lists");
}
/// Both owners plus the modelled constructs either grammar spells, so
/// a new owner among them is what the assertion catches.
const WORD_LIST_OWNER_FIXTURE: &[u8] = b"proc p {x} { puts $x }\n\
namespace eval ns { puts hi }\n\
namespace export {a b}\n\
if {$x} { puts a } else { puts b }\n\
while {$x} { puts a }\n\
foreach i {1 2} { puts $i }\n\
catch { puts a } msg\n\
eval {puts hi}\n\
switch $v { p {puts a} }\n\
lappend l {c d}\n";
#[test]
#[cfg(feature = "tcl")]
fn tcl_a_word_list_is_owned_only_by_a_command_or_a_namespace() {
assert_word_list_owners::<crate::langs::TclCode>(
"tcl",
WORD_LIST_OWNER_FIXTURE,
&crate::lang_helpers::tcl::BRACED_WORD_KINDS,
);
}
#[test]
#[cfg(feature = "irules")]
fn irules_a_word_list_is_owned_only_by_a_command_or_a_namespace() {
assert_word_list_owners::<crate::langs::IrulesCode>(
"irules",
WORD_LIST_OWNER_FIXTURE,
&crate::lang_helpers::irules::BRACED_WORD_KINDS,
);
}
}