@@ -3115,6 +3115,74 @@ TEST(complexity_go_method_receiver_self_recursion) {
31153115 PASS ();
31163116}
31173117
3118+ /* #876: the delegation/store bucket left open after #599 — same-named calls
3119+ * whose receiver is a CALL RESULT (_get_store().get), a module singleton
3120+ * (_default.check), or a local cursor inside a loop (cur.execute) are not
3121+ * self-recursion. All three are the reporter's exact v0.8.1 false positives;
3122+ * fixed by the receiver-aware narrowing (87091ed), guarded here so the
3123+ * bucket never regresses. */
3124+ TEST (complexity_delegation_receivers_not_recursive_issue876 ) {
3125+ /* Store wrapper: _get_store().get() inside get — receiver is the call
3126+ * result, a different object. */
3127+ CBMFileResult * r = extract ("def _get_store():\n"
3128+ " return object()\n"
3129+ "\n"
3130+ "def get(collection, record_id):\n"
3131+ " rec = _get_store().get(collection, record_id)\n"
3132+ " return rec\n" ,
3133+ CBM_LANG_PYTHON , "t" , "soil.py" );
3134+ ASSERT_NOT_NULL (r );
3135+ ASSERT_FALSE (r -> has_error );
3136+ const CBMDefinition * d = find_def (r , "get" );
3137+ ASSERT_NOT_NULL (d );
3138+ ASSERT_FALSE (d -> is_recursive ); /* _get_store() result is not this fn */
3139+ ASSERT_FALSE (d -> unguarded_recursion );
3140+ cbm_free_result (r );
3141+
3142+ /* Module-singleton delegation: _default.check() inside check. */
3143+ r = extract ("class _Checker:\n"
3144+ " def check(self, app_id, tool_name):\n"
3145+ " return (True, \"ok\")\n"
3146+ "\n"
3147+ "_default = _Checker()\n"
3148+ "\n"
3149+ "def check(app_id, tool_name):\n"
3150+ " return _default.check(app_id, tool_name)\n" ,
3151+ CBM_LANG_PYTHON , "t" , "gleipnir.py" );
3152+ ASSERT_NOT_NULL (r );
3153+ ASSERT_FALSE (r -> has_error );
3154+ /* The module-level wrapper (not the method) must stay clean. */
3155+ d = NULL ;
3156+ for (int i = 0 ; i < r -> defs .count ; i ++ ) {
3157+ if (r -> defs .items [i ].name && strcmp (r -> defs .items [i ].name , "check" ) == 0 &&
3158+ strcmp (r -> defs .items [i ].label , "Function" ) == 0 ) {
3159+ d = & r -> defs .items [i ];
3160+ }
3161+ }
3162+ ASSERT_NOT_NULL (d );
3163+ ASSERT_FALSE (d -> is_recursive ); /* _default is not this fn */
3164+ ASSERT_FALSE (d -> unguarded_recursion );
3165+ cbm_free_result (r );
3166+
3167+ /* Same-named method on a local, called inside a loop (willow.nuke case):
3168+ * neither recursive nor recursion_in_loop. */
3169+ r = extract ("def execute(conn, paths):\n"
3170+ " for p in paths:\n"
3171+ " cur = conn.cursor()\n"
3172+ " cur.execute(\"DELETE FROM t WHERE p = %s\", (p,))\n"
3173+ " return True\n" ,
3174+ CBM_LANG_PYTHON , "t" , "nuke.py" );
3175+ ASSERT_NOT_NULL (r );
3176+ ASSERT_FALSE (r -> has_error );
3177+ d = find_def (r , "execute" );
3178+ ASSERT_NOT_NULL (d );
3179+ ASSERT_FALSE (d -> is_recursive ); /* cur is not this fn */
3180+ ASSERT_FALSE (d -> unguarded_recursion );
3181+ ASSERT_FALSE (d -> recursion_in_loop );
3182+ cbm_free_result (r );
3183+ PASS ();
3184+ }
3185+
31183186/* Deep chained member access + parameter count structure smells. */
31193187TEST (complexity_access_depth_and_params ) {
31203188 CBMFileResult * r = extract ("package p\n"
@@ -3736,6 +3804,7 @@ SUITE(extraction) {
37363804 RUN_TEST (complexity_self_receiver_still_recursive );
37373805 RUN_TEST (complexity_chained_receiver_not_self );
37383806 RUN_TEST (complexity_go_method_receiver_self_recursion );
3807+ RUN_TEST (complexity_delegation_receivers_not_recursive_issue876 );
37393808 RUN_TEST (complexity_access_depth_and_params );
37403809 RUN_TEST (walk_defs_no_truncation_over_4096_issue668 );
37413810 RUN_TEST (extract_rust_test_attr_marks_is_test_issue855 );
0 commit comments