@@ -2893,6 +2893,277 @@ def extract_swift(path: Path) -> dict:
28932893 return _extract_generic (path , _SWIFT_CONFIG )
28942894
28952895
2896+ # ── DM (BYOND DreamMaker) extractor (custom walk) ────────────────────────────
2897+ # DM identity is path-based (`/datum/object/proc/New()`), not block-based, so
2898+ # the generic class-body walker doesn't fit.
2899+
2900+ def extract_dm (path : Path ) -> dict :
2901+ """Extract types, procs, includes, and calls from a .dm/.dme file."""
2902+ try :
2903+ import tree_sitter_dm as tsdm
2904+ from tree_sitter import Language , Parser
2905+ except ImportError :
2906+ return {"nodes" : [], "edges" : [], "error" : "tree-sitter-dm not installed" }
2907+ try :
2908+ language = Language (tsdm .language ())
2909+ parser = Parser (language )
2910+ source = path .read_bytes ()
2911+ tree = parser .parse (source )
2912+ root = tree .root_node
2913+ except Exception as e :
2914+ return {"nodes" : [], "edges" : [], "error" : str (e )}
2915+
2916+ stem = _file_stem (path )
2917+ str_path = str (path )
2918+ nodes : list [dict ] = []
2919+ edges : list [dict ] = []
2920+ seen_ids : set [str ] = set ()
2921+ function_bodies : list [tuple [str , Any , "str | None" ]] = []
2922+
2923+ def add_node (nid : str , label : str , line : int ) -> None :
2924+ if nid and nid not in seen_ids :
2925+ seen_ids .add (nid )
2926+ nodes .append ({"id" : nid , "label" : label , "file_type" : "code" ,
2927+ "source_file" : str_path , "source_location" : f"L{ line } " })
2928+
2929+ def add_edge (src : str , tgt : str , relation : str , line : int ,
2930+ confidence : str = "EXTRACTED" , weight : float = 1.0 ,
2931+ context : str | None = None ) -> None :
2932+ if not src or not tgt or src == tgt :
2933+ return
2934+ edge = {"source" : src , "target" : tgt , "relation" : relation ,
2935+ "confidence" : confidence , "source_file" : str_path ,
2936+ "source_location" : f"L{ line } " , "weight" : weight }
2937+ if context :
2938+ edge ["context" ] = context
2939+ edges .append (edge )
2940+
2941+ file_nid = _make_id (str (path ))
2942+ add_node (file_nid , path .name , 1 )
2943+
2944+ def _type_path_text (node ) -> str :
2945+ return _read_text (node , source ).strip ()
2946+
2947+ def _ensure_type (path_text : str , line : int ) -> str :
2948+ nid = _make_id (stem , path_text )
2949+ add_node (nid , path_text , line )
2950+ return nid
2951+
2952+ def _find_child (node , type_name : str ):
2953+ for c in node .children :
2954+ if c .type == type_name :
2955+ return c
2956+ return None
2957+
2958+ def _read_include_path (file_node ) -> str :
2959+ """Pull the inner path out of a string_literal or file_literal."""
2960+ if file_node is None :
2961+ return ""
2962+ if file_node .type == "string_literal" :
2963+ parts = []
2964+ for c in file_node .children :
2965+ if c .type == "string_content" :
2966+ parts .append (_read_text (c , source ))
2967+ return "" .join (parts )
2968+ return _read_text (file_node , source ).strip ("'\" " )
2969+
2970+ def walk (node , parent_type_path : "str | None" = None ,
2971+ parent_type_nid : "str | None" = None ) -> None :
2972+ t = node .type
2973+ line = node .start_point [0 ] + 1
2974+
2975+ if t == "preproc_include" :
2976+ file_node = node .child_by_field_name ("file" )
2977+ raw = _read_include_path (file_node )
2978+ if raw :
2979+ # BYOND projects on Windows author includes with backslashes.
2980+ norm = raw .replace ("\\ " , "/" ).lstrip ("./" )
2981+ resolved = (path .parent / norm ).resolve ()
2982+ edge = {
2983+ "source" : file_nid ,
2984+ "target" : _make_id (str (resolved )) if resolved .exists () else _make_id (norm ),
2985+ "relation" : "imports_from" if resolved .exists () else "imports" ,
2986+ "context" : "import" ,
2987+ "confidence" : "EXTRACTED" ,
2988+ "source_file" : str_path ,
2989+ "source_location" : f"L{ line } " ,
2990+ "weight" : 1.0 ,
2991+ }
2992+ # If the include doesn't exist on disk it's a BYOND stdlib or
2993+ # external dep; mark it so downstream consumers reading raw
2994+ # extractions can skip without re-deriving that.
2995+ if not resolved .exists ():
2996+ edge ["external" ] = True
2997+ edges .append (edge )
2998+ return
2999+
3000+ if t == "type_definition" :
3001+ tp_node = _find_child (node , "type_path" )
3002+ if tp_node is None :
3003+ return
3004+ type_path_str = _type_path_text (tp_node )
3005+ type_nid = _ensure_type (type_path_str , line )
3006+ add_edge (file_nid , type_nid , "contains" , line )
3007+ body = _find_child (node , "type_body" )
3008+ if body is not None :
3009+ for c in body .children :
3010+ walk (c , parent_type_path = type_path_str , parent_type_nid = type_nid )
3011+ return
3012+
3013+ if t in ("type_body_intended" , "type_body_braced" ):
3014+ for c in node .children :
3015+ walk (c , parent_type_path , parent_type_nid )
3016+ return
3017+
3018+ if t in ("type_proc_definition" , "type_proc_override" ):
3019+ if parent_type_nid is None or parent_type_path is None :
3020+ return
3021+ name_node = node .child_by_field_name ("name" )
3022+ if name_node is None :
3023+ return
3024+ proc_name = _read_text (name_node , source )
3025+ proc_nid = _make_id (stem , parent_type_path , proc_name )
3026+ add_node (proc_nid , f"{ parent_type_path } /{ proc_name } ()" , line )
3027+ add_edge (parent_type_nid , proc_nid , "method" , line )
3028+ block = _find_child (node , "block" )
3029+ if block is not None :
3030+ function_bodies .append ((proc_nid , block , parent_type_path ))
3031+ return
3032+
3033+ # `/proc/foo()`, `/datum/foo/proc/bar()`, or `/datum/foo/bar()` override.
3034+ if t in ("proc_definition" , "proc_override" ):
3035+ tp_node = _find_child (node , "type_path" )
3036+ owner_path : "str | None" = None
3037+ owner_nid : "str | None" = None
3038+ if tp_node is not None :
3039+ owner_path = _type_path_text (tp_node )
3040+ owner_nid = _ensure_type (owner_path , line )
3041+ add_edge (file_nid , owner_nid , "contains" , line )
3042+ name_node = node .child_by_field_name ("name" )
3043+ if name_node is None :
3044+ return
3045+ proc_name = _read_text (name_node , source )
3046+ if owner_path and owner_nid :
3047+ proc_nid = _make_id (stem , owner_path , proc_name )
3048+ add_node (proc_nid , f"{ owner_path } /{ proc_name } ()" , line )
3049+ add_edge (owner_nid , proc_nid , "method" , line )
3050+ else :
3051+ proc_nid = _make_id (stem , proc_name )
3052+ add_node (proc_nid , f"{ proc_name } ()" , line )
3053+ add_edge (file_nid , proc_nid , "contains" , line )
3054+ block = _find_child (node , "block" )
3055+ if block is not None :
3056+ function_bodies .append ((proc_nid , block , owner_path ))
3057+ return
3058+
3059+ # The grammar mis-parses operator overload names (`+`, `[]`, …), so skip
3060+ # rather than emit half-broken nodes.
3061+ if t in ("operator_override" , "type_operator_override" ):
3062+ return
3063+
3064+ for child in node .children :
3065+ walk (child , parent_type_path , parent_type_nid )
3066+
3067+ walk (root )
3068+
3069+ # ── Call-graph pass ───────────────────────────────────────────────────────
3070+ # Two indexes: last-segment for proc lookups, full-path for `new`. DM has
3071+ # heavy method overriding by path (e.g. 8 same-named overrides in one file),
3072+ # so on last-segment collision we resolve only when there's exactly one
3073+ # match — same rule the cross-file resolver uses.
3074+ label_to_nids : dict [str , list [str ]] = {}
3075+ path_to_nids : dict [str , list [str ]] = {}
3076+ for n in nodes :
3077+ label = n ["label" ].strip ("()" )
3078+ last = label .rsplit ("/" , 1 )[- 1 ] if "/" in label else label
3079+ if last :
3080+ label_to_nids .setdefault (last .lower (), []).append (n ["id" ])
3081+ if label .startswith ("/" ):
3082+ path_to_nids .setdefault (label .lower (), []).append (n ["id" ])
3083+
3084+ seen_call_pairs : set [tuple [str , str ]] = set ()
3085+ raw_calls : list [dict ] = []
3086+
3087+ def _emit_call (caller_nid : str , callee : str , line : int , is_member : bool ) -> None :
3088+ candidates = label_to_nids .get (callee .lower (), [])
3089+ tgt_nid = candidates [0 ] if len (candidates ) == 1 else None
3090+ if tgt_nid and tgt_nid != caller_nid :
3091+ pair = (caller_nid , tgt_nid )
3092+ if pair in seen_call_pairs :
3093+ return
3094+ seen_call_pairs .add (pair )
3095+ edges .append ({
3096+ "source" : caller_nid ,
3097+ "target" : tgt_nid ,
3098+ "relation" : "calls" ,
3099+ "context" : "call" ,
3100+ "confidence" : "EXTRACTED" ,
3101+ "source_file" : str_path ,
3102+ "source_location" : f"L{ line } " ,
3103+ "weight" : 1.0 ,
3104+ })
3105+ else :
3106+ raw_calls .append ({
3107+ "caller_nid" : caller_nid ,
3108+ "callee" : callee ,
3109+ "is_member_call" : is_member ,
3110+ "source_file" : str_path ,
3111+ "source_location" : f"L{ line } " ,
3112+ })
3113+
3114+ def walk_calls (body_node , caller_nid : str ) -> None :
3115+ if body_node is None :
3116+ return
3117+ t = body_node .type
3118+ if t in ("proc_definition" , "proc_override" , "type_proc_definition" ,
3119+ "type_proc_override" , "type_definition" ):
3120+ return
3121+ if t == "call_expression" :
3122+ name_node = body_node .child_by_field_name ("name" )
3123+ if name_node is not None :
3124+ callee = _read_text (name_node , source )
3125+ # `..` is a super-call; resolving it needs corpus-wide type
3126+ # hierarchy we don't have here, so skip rather than guess.
3127+ if callee and callee != ".." :
3128+ _emit_call (caller_nid , callee , body_node .start_point [0 ] + 1 ,
3129+ is_member = False )
3130+ elif t == "field_proc_expression" :
3131+ proc_field = body_node .child_by_field_name ("proc" )
3132+ if proc_field is not None :
3133+ callee = _read_text (proc_field , source )
3134+ if callee :
3135+ _emit_call (caller_nid , callee , body_node .start_point [0 ] + 1 ,
3136+ is_member = True )
3137+ elif t == "new_expression" :
3138+ tp_node = _find_child (body_node , "type_path" )
3139+ if tp_node is not None :
3140+ target_text = _type_path_text (tp_node )
3141+ candidates = path_to_nids .get (target_text .lower (), [])
3142+ tgt_nid = candidates [0 ] if len (candidates ) == 1 else None
3143+ if tgt_nid and tgt_nid != caller_nid :
3144+ pair = (caller_nid , tgt_nid )
3145+ if pair not in seen_call_pairs :
3146+ seen_call_pairs .add (pair )
3147+ edges .append ({
3148+ "source" : caller_nid ,
3149+ "target" : tgt_nid ,
3150+ "relation" : "instantiates" ,
3151+ "context" : "call" ,
3152+ "confidence" : "EXTRACTED" ,
3153+ "source_file" : str_path ,
3154+ "source_location" : f"L{ body_node .start_point [0 ] + 1 } " ,
3155+ "weight" : 1.0 ,
3156+ })
3157+
3158+ for child in body_node .children :
3159+ walk_calls (child , caller_nid )
3160+
3161+ for proc_nid , block , _owner_path in function_bodies :
3162+ walk_calls (block , proc_nid )
3163+
3164+ return {"nodes" : nodes , "edges" : edges , "raw_calls" : raw_calls }
3165+
3166+
28963167# ── Julia extractor (custom walk) ────────────────────────────────────────────
28973168
28983169def extract_julia (path : Path ) -> dict :
@@ -6062,6 +6333,8 @@ def walk_object(obj_node, parent_nid: str, parent_key: str | None,
60626333 ".sh" : extract_bash ,
60636334 ".bash" : extract_bash ,
60646335 ".json" : extract_json ,
6336+ ".dm" : extract_dm ,
6337+ ".dme" : extract_dm ,
60656338}
60666339
60676340
0 commit comments