@@ -29,6 +29,10 @@ const SYM_SEMI: Int = 59
2929// '?' — the try-operator symbol code (single-char symbol, val = code point).
3030const SYM_QUESTION: Int = 63
3131
32+ // '|' — the closure-parameter delimiter (single-char symbol). Used by the RS0201
33+ // arg-splitter to skip commas inside a closure param list `|a, b|`.
34+ const SYM_PIPE: Int = 124
35+
3236fn tk_text(toks: read List<Tok >, i: read Int) -> String {
3337 if i < 0 { return "" }
3438 if i >= List.len(list: read toks) { return "" }
@@ -2913,6 +2917,179 @@ fn fn_invalid_try(toks: read List<Tok>, start: read Int, declared: read Set<Stri
29132917 return hit
29142918}
29152919
2920+ // ---------------------------------------------------------------------------
2921+ // RS0201 UNNAMED_ARGUMENT (check_argument_naming, checks/calls.rs).
2922+ //
2923+ // A call argument passed WITHOUT a `name:` label where the callee requires named
2924+ // arguments. Named args are REQUIRED for public same-file functions, and for
2925+ // core/native/stdlib qualified calls (`Ns.method(...)`); they are NOT required
2926+ // for receiver-call shorthand (`value.method(...)`), private same-file helpers,
2927+ // enum-variant constructors (`Ok`/`Some`/user variants), or type constructors
2928+ // (which also permit field shorthand). The parser (syntax/parser/expr.rs
2929+ // is_qualified_namespace_receiver) treats an UPPERCASE dotted head as a qualified
2930+ // namespace call and a lowercase head as a receiver call — replicated here by the
2931+ // first-character-uppercase test. FILE-LEVEL: emit once if ANY resolved call site
2932+ // carries an unnamed argument. Conservative on anything not confidently a
2933+ // named-required call (skip => no false positive).
2934+ // ---------------------------------------------------------------------------
2935+
2936+ // Index of the `<` that matches the `>` at `closePos`, scanning backwards, or -1.
2937+ fn back_match_angle(toks: read List<Tok >, closePos: read Int) -> Int {
2938+ let mut depth = 0
2939+ let mut k = closePos
2940+ let mut res = -1
2941+ while k >= 0 && res < 0 {
2942+ if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
2943+ depth = depth + 1
2944+ } else if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
2945+ depth = depth - 1
2946+ if depth == 0 { res = k }
2947+ }
2948+ k = k - 1
2949+ }
2950+ return res
2951+ }
2952+
2953+ // Collect same-file fn name sets: `pubFns` = unqualified fn names declared `pub`;
2954+ // `dottedFns` = full dotted names of same-file dotted fns (e.g. `Cache.put`), used
2955+ // to treat a qualified call to a same-file method as positional-allowed.
2956+ fn collect_call_fn_sigs(toks: read List<Tok >, pubFns: mut Set<String >, dottedFns: mut Set<String >) -> Unit {
2957+ let n = List.len(list: read toks)
2958+ let mut i = 0
2959+ while i < n {
2960+ if at_ident(toks: read toks, i: read i, wid: read WORD_FN) {
2961+ let nameIdx = i + 1
2962+ if is_ident_tok(toks: read toks, i: read nameIdx) {
2963+ let full = dotted_name_text(toks: read toks, i: read nameIdx)
2964+ let isDotted = at_symbol(toks: read toks, i: read (nameIdx + 1), code: read SYM_DOT)
2965+ let mut isPub = false
2966+ let mut b = i - 1
2967+ let mut scanning = true
2968+ while scanning {
2969+ if at_ident(toks: read toks, i: read b, wid: read WORD_PUB) {
2970+ isPub = true
2971+ b = b - 1
2972+ } else if at_ident(toks: read toks, i: read b, wid: read WORD_ASYNC) || at_ident(toks: read toks, i: read b, wid: read WORD_NATIVE) {
2973+ b = b - 1
2974+ } else {
2975+ scanning = false
2976+ }
2977+ }
2978+ if isDotted {
2979+ Set.insert<String >(set: mut dottedFns, value: read full)
2980+ } else if isPub {
2981+ Set.insert<String >(set: mut pubFns, value: read full)
2982+ }
2983+ }
2984+ }
2985+ i = i + 1
2986+ }
2987+ return Unit
2988+ }
2989+
2990+ // Core function-namespaces whose qualified calls (`Ns.method(...)`) require named
2991+ // arguments. Curated (measure-first) to the namespaces the corpus oracle actually
2992+ // flags: broadening to every stdlib/builtin type would risk false positives on
2993+ // stdlib sum-variant construction (`JsonValue.String(...)`) and on unknown/imported
2994+ // namespaces (`ChatMessage.system(...)`, which the oracle leaves as UNKNOWN_CALLEE
2995+ // with no naming diagnostic). See checks/calls.rs check_argument_naming.
2996+ fn is_core_named_namespace(head: read String) -> Bool {
2997+ return head == "String" || head == "Image" || head == "Log"
2998+ }
2999+
3000+ // True iff the call whose arg-list `(` is at `op` requires named arguments: a
3001+ // public same-file unqualified fn, or a core qualified call to a curated
3002+ // function-namespace. Bails (false) on receiver calls, private same-file helpers,
3003+ // variant/type constructors, and any callee shape it cannot resolve confidently.
3004+ fn call_requires_named(toks: read List<Tok >, op: read Int, pubFns: read Set<String >, dottedFns: read Set<String >) -> Bool {
3005+ let mut nameLast = op - 1
3006+ if at_symbol(toks: read toks, i: read nameLast, code: read SYM_RANGLE) {
3007+ let openAngle = back_match_angle(toks: read toks, closePos: read nameLast)
3008+ if openAngle < 0 { return false }
3009+ nameLast = openAngle - 1
3010+ }
3011+ if is_ident_tok(toks: read toks, i: read nameLast) == false { return false }
3012+ if at_symbol(toks: read toks, i: read (nameLast - 1), code: read SYM_DOT) {
3013+ if is_ident_tok(toks: read toks, i: read (nameLast - 2)) == false { return false }
3014+ let dotHead = nameLast - 2
3015+ if at_symbol(toks: read toks, i: read (dotHead - 1), code: read SYM_DOT) { return false }
3016+ let head = tk_text(toks: read toks, i: read dotHead)
3017+ let full = String.concat(left: read String.concat(left: read head, right: read "."), right: read tk_text(toks: read toks, i: read nameLast))
3018+ if Set.contains<String >(set: read dottedFns, value: read full) { return false }
3019+ return is_core_named_namespace(head: read head)
3020+ }
3021+ let name = tk_text(toks: read toks, i: read nameLast)
3022+ return Set.contains<String >(set: read pubFns, value: read name)
3023+ }
3024+
3025+ // True iff a call-arg segment [s,e) is an unnamed argument: empty (a malformed
3026+ // slot from a leading/double comma), or a value not prefixed by a `name:` label.
3027+ fn seg_is_unnamed(toks: read List<Tok >, s: read Int, e: read Int) -> Bool {
3028+ if s >= e { return true }
3029+ if is_ident_tok(toks: read toks, i: read s) && at_symbol(toks: read toks, i: read (s + 1), code: read SYM_COLON) {
3030+ return false
3031+ }
3032+ return true
3033+ }
3034+
3035+ // True iff the arg list [op+1, close) has any unnamed argument. A single trailing
3036+ // empty segment (trailing comma) does NOT count; empty leading/middle segments do.
3037+ fn args_have_unnamed(toks: read List<Tok >, op: read Int, close: read Int) -> Bool {
3038+ let mut k = op + 1
3039+ let mut depth = 0
3040+ let mut angle = 0
3041+ let mut inPipe = false
3042+ let mut segStart = op + 1
3043+ let mut hit = false
3044+ while k < close {
3045+ let opener = at_symbol(toks: read toks, i: read k, code: read SYM_LPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_LBRACKET) || at_symbol(toks: read toks, i: read k, code: read SYM_LBRACE)
3046+ let closer = at_symbol(toks: read toks, i: read k, code: read SYM_RPAREN) || at_symbol(toks: read toks, i: read k, code: read SYM_RBRACKET) || at_symbol(toks: read toks, i: read k, code: read SYM_RBRACE)
3047+ if opener {
3048+ depth = depth + 1
3049+ } else if closer {
3050+ if depth > 0 { depth = depth - 1 }
3051+ } else if at_symbol(toks: read toks, i: read k, code: read SYM_LANGLE) {
3052+ angle = angle + 1
3053+ } else if at_symbol(toks: read toks, i: read k, code: read SYM_RANGLE) {
3054+ if angle > 0 { angle = angle - 1 }
3055+ } else if depth == 0 && angle == 0 && at_symbol(toks: read toks, i: read k, code: read SYM_PIPE) {
3056+ inPipe = inPipe == false
3057+ } else if depth == 0 && angle == 0 && inPipe == false && at_symbol(toks: read toks, i: read k, code: read SYM_COMMA) {
3058+ if seg_is_unnamed(toks: read toks, s: read segStart, e: read k) { hit = true }
3059+ segStart = k + 1
3060+ }
3061+ k = k + 1
3062+ }
3063+ if segStart < close {
3064+ if seg_is_unnamed(toks: read toks, s: read segStart, e: read close) { hit = true }
3065+ }
3066+ return hit
3067+ }
3068+
3069+ // RS0201 over a fn body [sigEnd+1, bodyClose): any resolved named-required call
3070+ // site with an unnamed argument.
3071+ fn fn_has_unnamed_arg(toks: read List<Tok >, start: read Int, pubFns: read Set<String >, dottedFns: read Set<String >) -> Bool {
3072+ let ns = fn_name_start(toks: read toks, i: read start)
3073+ let sigEnd = function_signature_end(toks: read toks, start: read (ns - 1))
3074+ if at_symbol(toks: read toks, i: read sigEnd, code: read SYM_LBRACE) == false { return false }
3075+ let bodyClose = find_matching(toks: read toks, open: read sigEnd, openSym: read SYM_LBRACE, closeSym: read SYM_RBRACE)
3076+ if bodyClose < 0 { return false }
3077+ let mut op = sigEnd + 1
3078+ let mut hit = false
3079+ while op < bodyClose {
3080+ if at_symbol(toks: read toks, i: read op, code: read SYM_LPAREN) {
3081+ if call_requires_named(toks: read toks, op: read op, pubFns: read pubFns, dottedFns: read dottedFns) {
3082+ let close = find_matching(toks: read toks, open: read op, openSym: read SYM_LPAREN, closeSym: read SYM_RPAREN)
3083+ if close >= 0 && close <= bodyClose {
3084+ if args_have_unnamed(toks: read toks, op: read op, close: read close) { hit = true }
3085+ }
3086+ }
3087+ }
3088+ op = op + 1
3089+ }
3090+ return hit
3091+ }
3092+
29163093fn main() -> Unit {
29173094 let source = Args.get_or_default(index: 0, default: read "")
29183095 let toks = tokenize(source: read source)
@@ -2971,6 +3148,12 @@ fn main() -> Unit {
29713148 let mut allFns = Set<String >.new()
29723149 let mut noBlockFns = Set<String >.new()
29733150 let mut noPanicFns = Set<String >.new()
3151+ // RS0201: public unqualified fn names + dotted (method) fn names, for the
3152+ // named-argument requirement.
3153+ let mut pubFns = Set<String >.new()
3154+ let mut dottedFns = Set<String >.new()
3155+ collect_call_fn_sigs(toks: read toks, pubFns: mut pubFns, dottedFns: mut dottedFns)
3156+ let mut unnamedArg = false
29743157 // Signature table (cross-function): same-file async fn names, for RS0022.
29753158 let mut asyncFns = Set<String >.new()
29763159 collect_rs0009(toks: read toks, resources: mut resources, sumVariants: mut sumVariants, pureFns: mut pureFns, allFns: mut allFns, noBlockFns: mut noBlockFns, noPanicFns: mut noPanicFns, asyncFns: mut asyncFns)
@@ -3152,6 +3335,7 @@ fn main() -> Unit {
31523335 if fn_has_nonexhaustive_match(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { nonExhaustive = true }
31533336 if fn_has_bad_await(toks: read toks, start: read i) { badAwait = true }
31543337 if fn_invalid_try(toks: read toks, start: read i, declared: read declared, allFns: read allFns) { invalidTry = true }
3338+ if fn_has_unnamed_arg(toks: read toks, start: read i, pubFns: read pubFns, dottedFns: read dottedFns) { unnamedArg = true }
31553339 if fn_has_fd_surface(toks: read toks, start: read i) { fdSurface = true }
31563340 // RS0027: an unknown protocol in this fn's generic bounds.
31573341 if generics_unknown_protocol(toks: read toks, nameEnd: read dotted_end(toks: read toks, i: read ns), protocols: read protocols) { unknownProtocol = true }
@@ -3236,6 +3420,9 @@ fn main() -> Unit {
32363420 if invalidTry {
32373421 Log.write(message: read "RS0013")
32383422 }
3423+ if unnamedArg {
3424+ Log.write(message: read "RS0201")
3425+ }
32393426 if asyncNotConsumed {
32403427 Log.write(message: read "RS0022")
32413428 }
@@ -3279,7 +3466,7 @@ fn main() -> Unit {
32793466 if featureViol {
32803467 Log.write(message: read "RS0101")
32813468 }
3282- let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || invalidTry || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed || featureViol
3469+ let anyDiag = found || hasUnknownFeat || hasDupFeat || headerCount >= 2 || missingReturn || untypedParam || hasProfile || shareParam || unknownEffect || removedRuntime || invalidSelf || outOfRangeInt || retainsBad || unknownType || missingEffect || pureBad || nonExhaustive || badAwait || invalidTry || fdSurface || lowerConflict || unknownProtocol || noallocAlloc || noBlockCall || noPanicCall || asyncNotConsumed || featureViol || unnamedArg
32833470 if anyDiag == false {
32843471 Log.write(message: read "CLEAN")
32853472 }
0 commit comments