-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraphql_module.py
More file actions
1618 lines (1343 loc) · 73.2 KB
/
Copy pathgraphql_module.py
File metadata and controls
1618 lines (1343 loc) · 73.2 KB
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
# -*- coding: utf-8 -*-
"""
GraphQL Module - File-level parsing logic
This module encapsulates all parsing logic for a single GraphQL source file.
It follows a modular, extensible architecture:
- Light parsing (Pass 1): Automatic extraction of global structures
- Full parsing (Pass 2): MANUAL IMPLEMENTATION REQUIRED for link detection
IMPORTANT:
----------
Pass 1 (object detection) is fully implemented and automatic.
Pass 2 (link detection) requires YOU to implement technology-specific logic.
The following methods are SKELETONS that you must implement:
- full_parse(): Detect function/method calls
- resolve(): Resolve call targets to objects
- save_links(): Create links using the CAST SDK
See each method's docstring for detailed implementation guidance.
Python 3.4+ compatible.
"""
from cast.analysers import log, CustomObject, create_link, Bookmark
import os
import re
from collections import defaultdict
# =============================================================================
# OBJECT HIERARCHY CONFIGURATION
# =============================================================================
# This configuration is generated from config_input.json
# It defines:
# - Object types and their parent relationships
# - Pattern keys that detect each object type
#
# Format:
# 'ObjectType': {
# 'parent': 'ParentType' or 'file', # Where this object can be contained
# 'pattern_keys': ['pattern1', ...] # Grammar patterns that detect this type
# }
# =============================================================================
OBJECTS_CONFIG = {
'Program': {'parent': 'file', 'pattern_keys': []},
'Schema': {'parent': 'file', 'pattern_keys': ['schema_def']},
'Type': {'parent': 'Schema', 'pattern_keys': ['type_def', 'type_def_implements', 'type_def_implements_multiple', 'type_extend', 'type_extend_implements', 'type_def_description']},
'Interface': {'parent': 'Schema', 'pattern_keys': ['interface_def', 'interface_def_implements', 'interface_extend', 'interface_def_description']},
'Enum': {'parent': 'Schema', 'pattern_keys': ['enum_def', 'enum_extend', 'enum_def_description']},
'EnumValue': {'parent': 'Enum', 'pattern_keys': ['enum_value', 'enum_value_deprecated', 'enum_value_directive', 'enum_value_description']},
'Input': {'parent': 'Schema', 'pattern_keys': ['input_def', 'input_extend', 'input_def_description']},
'Union': {'parent': 'Schema', 'pattern_keys': ['union_def', 'union_extend', 'union_def_description']},
'Scalar': {'parent': 'Schema', 'pattern_keys': ['scalar_def', 'scalar_directive', 'scalar_def_description']},
'Directive': {'parent': 'Schema', 'pattern_keys': ['directive_def', 'directive_def_with_args', 'directive_def_repeatable']},
'Field': {'parent': 'Type', 'pattern_keys': ['field_def', 'field_def_with_args', 'field_def_non_null', 'field_def_list', 'field_def_list_non_null', 'field_def_deprecated', 'field_def_directive', 'field_def_description']},
'Query': {'parent': 'Schema', 'pattern_keys': ['query_operation', 'query_operation_named', 'query_operation_with_vars', 'query_operation_anonymous']},
'Mutation': {'parent': 'Schema', 'pattern_keys': ['mutation_operation', 'mutation_operation_named', 'mutation_operation_with_vars']},
'Subscription': {'parent': 'Schema', 'pattern_keys': ['subscription_operation', 'subscription_operation_named', 'subscription_operation_with_vars']},
'Fragment': {'parent': 'Schema', 'pattern_keys': ['fragment_def', 'fragment_def_directive', 'inline_fragment']},
'Variable': {'parent': 'Query', 'pattern_keys': ['variable_def', 'variable_def_default', 'variable_def_non_null']},
}
# Build reverse mapping: pattern_key -> object_type
PATTERN_TO_OBJECT_TYPE = {}
for obj_type, obj_def in OBJECTS_CONFIG.items():
for pattern_key in obj_def.get('pattern_keys', []):
PATTERN_TO_OBJECT_TYPE[pattern_key] = obj_type
# Build parent hierarchy for containment tracking
OBJECT_PARENTS = {obj_type: obj_def['parent'] for obj_type, obj_def in OBJECTS_CONFIG.items()}
# =============================================================================
# PARSER REGISTRY - Extensibility point for custom parsers
# =============================================================================
class ParserRegistry:
"""
Registry for pluggable AST node handlers.
Allows users to extend parsing capabilities without modifying core code.
Register handlers for specific node types or patterns.
Usage:
registry = ParserRegistry()
registry.register('ClassDef', my_class_handler)
registry.register('FunctionDef', my_function_handler)
"""
def __init__(self):
self._handlers = defaultdict(list)
self._pattern_handlers = []
def register(self, node_type, handler):
"""
Register a handler for a specific node type.
Args:
node_type (str): AST node type name (e.g., 'ClassDef', 'FunctionDef')
handler (callable): Function(node, module) -> list of objects
"""
self._handlers[node_type].append(handler)
def register_pattern(self, pattern, handler):
"""
Register a handler for nodes matching a regex pattern.
Args:
pattern (str): Regex pattern to match against node types
handler (callable): Function(node, module) -> list of objects
"""
self._pattern_handlers.append((re.compile(pattern), handler))
def get_handlers(self, node_type):
"""Get all handlers for a node type."""
handlers = list(self._handlers.get(node_type, []))
for pattern, handler in self._pattern_handlers:
if pattern.match(node_type):
handlers.append(handler)
return handlers
# Global parser registry - extend this to add custom handlers
PARSER_REGISTRY = ParserRegistry()
# =============================================================================
# LIBRARY - Container for all modules
# =============================================================================
class GraphQLLibrary:
"""
Container for all GraphQL modules in the analysis.
This class stores all parsed modules and maintains a global symbol table
that can be used for cross-file resolution.
IMPORTANT: The resolve_symbol() method is an OPTIONAL HELPER UTILITY.
It attempts generic resolution which may or may not work for your
specific language.
When implementing link detection in the Module class, you can:
1. Use this helper method as a starting point
2. Ignore it and implement your own resolution logic
3. Extend it with technology-specific knowledge
Attributes:
modules (list): List of GraphQLModule instances
symbols (dict): Global symbol table {fullname: object}
symbols_by_name (dict): Maps short names to list of fullnames
module_by_path (dict): Maps file paths to modules
"""
def __init__(self):
self.modules = []
self.symbols = {} # {fullname: CustomObject}
self.symbols_by_name = defaultdict(list) # {short_name: [fullnames]}
self.module_by_path = {} # {path: module} for import resolution
def add_module(self, module):
"""Add a module to the library and register its objects."""
self.modules.append(module)
self.module_by_path[module.path] = module
# Register all objects from this module in the global symbol table
for fullname, obj in module.objects.items():
short_name = None
if hasattr(module, '_short_names') and fullname in module._short_names:
short_name = module._short_names[fullname]
self.register_symbol(fullname, obj, short_name)
def get_modules(self):
"""Get all modules in the library."""
return self.modules
def register_symbol(self, fullname, obj, short_name=None):
"""
Register a symbol in the global symbol table.
Args:
fullname (str): Fully qualified name of the symbol
obj: The CAST CustomObject
short_name (str, optional): Short name for resolution
"""
self.symbols[fullname] = obj
if short_name:
self.symbols_by_name[short_name].append(fullname)
def resolve_symbol(self, name, context_module=None, restrict_to_file=False, restrict_to_class=None):
"""
[OPTIONAL HELPER] Resolve a symbol name to its object.
This is a GENERIC resolution algorithm with strict rules to avoid
false positives. It may work for some languages but will likely need
customization for your specific technology.
Resolution strategy:
1. Exact fullname match (qualified calls like "ClassName.method")
2. Same-file match (prefer local definitions)
3. Cross-file match ONLY if there's exactly ONE candidate (no ambiguity)
Args:
name (str): Symbol name to resolve
context_module: Module providing context for resolution
restrict_to_file (bool): Only search in same file (for self/this calls)
restrict_to_class (str): Only search in specific class fullname prefix
Returns:
tuple: (CustomObject, fullname) or (None, None)
"""
# Try exact match first (handles qualified calls)
if name in self.symbols:
return self.symbols[name], name
# Try resolution by short name
if name in self.symbols_by_name:
candidates = self.symbols_by_name[name]
# If restricted to a specific class, filter candidates
if restrict_to_class:
class_candidates = [fn for fn in candidates if fn.startswith(restrict_to_class + '.')]
if len(class_candidates) == 1:
return self.symbols[class_candidates[0]], class_candidates[0]
return None, None # Multiple or no matches in class
# If restricted to same file, filter candidates
if restrict_to_file and context_module:
file_prefix = context_module.path + '.'
file_candidates = [fn for fn in candidates if fn.startswith(file_prefix)]
if len(file_candidates) == 1:
return self.symbols[file_candidates[0]], file_candidates[0]
return None, None # Multiple or no matches in file
# Prefer same-file match
if context_module:
file_prefix = context_module.path + '.'
file_candidates = [fn for fn in candidates if fn.startswith(file_prefix)]
if len(file_candidates) == 1:
return self.symbols[file_candidates[0]], file_candidates[0]
elif len(file_candidates) > 1:
# Multiple matches in same file - ambiguous, don't guess
return None, None
# Cross-file resolution: ONLY if exactly ONE candidate exists
# This prevents false positives when multiple files define same function
if len(candidates) == 1:
return self.symbols[candidates[0]], candidates[0]
else:
# Multiple candidates across files - too ambiguous, don't create link
return None, None
# Try suffix matching: look for fullnames ending with .name or :name
# This handles cases like resolving "new" to "UserService.new"
suffix_patterns = ['.' + name, ':' + name]
candidates = []
for fullname in self.symbols:
for suffix in suffix_patterns:
if fullname.endswith(suffix):
candidates.append(fullname)
break
if candidates:
# Prefer same-file match
if context_module:
file_prefix = context_module.path + '.'
file_candidates = [fn for fn in candidates if fn.startswith(file_prefix)]
if len(file_candidates) == 1:
return self.symbols[file_candidates[0]], file_candidates[0]
elif len(file_candidates) > 1:
return None, None # Ambiguous
# Only return if exactly one candidate
if len(candidates) == 1:
return self.symbols[candidates[0]], candidates[0]
return None, None
# =============================================================================
# AST NODE - Generic AST representation
# =============================================================================
class ASTNode:
"""
Generic AST node representation.
Provides a language-agnostic structure for representing parsed elements.
Can be extended or replaced with language-specific implementations.
Attributes:
type (str): Node type (e.g., 'class', 'function', 'method')
name (str): Node name
start_line (int): Starting line number
end_line (int): Ending line number
children (list): Child nodes
properties (dict): Additional node-specific properties
"""
def __init__(self, node_type, name=None, start_line=0, end_line=0):
self.type = node_type
self.name = name
self.start_line = start_line
self.end_line = end_line
self.children = []
self.properties = {}
self.parent = None
def add_child(self, child):
"""Add a child node."""
child.parent = self
self.children.append(child)
return child
def get_children_by_type(self, node_type):
"""Get all children of a specific type."""
return [c for c in self.children if c.type == node_type]
def walk(self):
"""
Generator that yields all nodes in the tree (depth-first).
Yields:
ASTNode: Each node in the tree
"""
yield self
for child in self.children:
for node in child.walk():
yield node
# =============================================================================
# MODULE CLASS - Core file parsing logic
# =============================================================================
class GraphQLModule:
"""
Encapsulates parsing logic for a single GraphQL source file.
Implements the 2-pass parsing architecture:
- light_parse(): Extract global structures (classes, functions)
- full_parse(): Deep analysis for references and calls (SKELETON - implement manually)
- resolve(): Resolve call targets (SKELETON - implement manually)
- save_links(): Create links using CAST SDK (SKELETON - implement manually)
Attributes:
path (str): Full path to the source file
file: CAST File object
ast (ASTNode): Parsed AST (available after light_parse)
objects (dict): Created CAST objects {fullname: CustomObject}
pending_links (list): Links to create during resolution
imported_symbols (dict): Tracked imports {name: fullname}
"""
def __init__(self, path, file=None):
"""
Initialize a module for a source file.
Args:
path (str): Full path to the source file
file: CAST File object (from start_file callback)
"""
self.path = path
self.file = file
self.ast = None
self.source_content = None
self.cleaned_source = None # Source with strings/comments removed
# Object storage
self.objects = {} # {fullname: CustomObject}
self.objects_by_type = defaultdict(list) # {type: [objects]}
self.object_lines = {} # {fullname: (start_line, end_line)} for caller resolution
# Links to create during resolution
self.pending_links = [] # [(caller_fullname, callee_name, link_type, line)]
# Unresolved calls for reporting
self.unresolved_calls = [] # [{file, line, code, callee, reason}]
# Import tracking (may be useful for link resolution)
self.imported_symbols = {} # {imported_name: source_module_hint}
self.import_statements = [] # Raw import statements for analysis
# Program-level object (container for file)
self.program = None
def get_path(self):
"""Get the file path."""
return self.path
def get_base_name(self):
"""Get the base filename without extension."""
return os.path.splitext(os.path.basename(self.path))[0]
def get_filename(self):
"""Get the filename with extension."""
return os.path.basename(self.path)
# =========================================================================
# SOURCE CLEANING - Remove strings and comments before parsing
# =========================================================================
def _clean_source_for_parsing(self, content):
"""
Remove strings and comments from source to avoid false positives.
This prevents detecting calls inside:
- String literals: "call helper() here"
- Comments: # call helper()
- Multi-line strings/comments
Preserves line structure (replaces with spaces of same length).
Args:
content (str): Raw source content
Returns:
str: Cleaned source with strings/comments replaced by spaces
"""
if not content:
return content
result = []
lines = content.splitlines(keepends=True)
in_multiline_string = False
in_multiline_comment = False
multiline_delim = None
# Get comment syntax from config
single_comment = '#'
for line in lines:
cleaned_line = []
i = 0
line_len = len(line)
while i < line_len:
# Check if we're ending a multiline construct
if in_multiline_string:
if line[i:i+len(multiline_delim)] == multiline_delim:
# End of multiline string
cleaned_line.append(' ' * len(multiline_delim))
i += len(multiline_delim)
in_multiline_string = False
multiline_delim = None
else:
cleaned_line.append(' ')
i += 1
continue
if in_multiline_comment:
if line[i:i+2] == '*/':
cleaned_line.append(' ')
i += 2
in_multiline_comment = False
else:
cleaned_line.append(' ')
i += 1
continue
char = line[i]
# Check for single-line comment
if single_comment and line[i:i+len(single_comment)] == single_comment:
# Replace rest of line with spaces
remaining = line_len - i
cleaned_line.append(' ' * remaining)
break
# Check for multi-line comment start /*
if line[i:i+2] == '/*':
in_multiline_comment = True
cleaned_line.append(' ')
i += 2
continue
# Check for triple-quoted strings (Python, GraphQL)
if line[i:i+3] in ('"""', "'''"):
delim = line[i:i+3]
# Look for end on same line
end_pos = line.find(delim, i+3)
if end_pos != -1:
# Single-line triple-quoted string
length = end_pos - i + 3
cleaned_line.append(' ' * length)
i = end_pos + 3
else:
# Start of multiline string
in_multiline_string = True
multiline_delim = delim
cleaned_line.append(' ')
i += 3
continue
# Check for regular strings
if char in ('"', "'"):
quote = char
j = i + 1
while j < line_len:
if line[j] == '\\' and j + 1 < line_len:
j += 2 # Skip escaped char
elif line[j] == quote:
j += 1
break
else:
j += 1
# Replace string content with spaces
length = j - i
cleaned_line.append(' ' * length)
i = j
continue
# Regular character - keep it
cleaned_line.append(char)
i += 1
result.append(''.join(cleaned_line))
return ''.join(result)
# =========================================================================
# IMPORT TRACKING - Detect and track import statements
# =========================================================================
def _extract_imports(self):
"""
Extract import statements from the source.
Detects common import patterns across languages:
- Python: import X, from X import Y
- JavaScript/TypeScript: import X from 'Y', require('Y')
- Go: import "path"
- etc.
Populates self.imported_symbols with {name: source_hint}
"""
if not self.source_content:
return
# Generic import patterns (covers most languages)
import_patterns = [
# Python: from module import name
r'^\s*from\s+([a-zA-Z_][\w.]*)\s+import\s+([a-zA-Z_][\w,\s*]*)',
# Python: import module
r'^\s*import\s+([a-zA-Z_][\w.]*)',
# JavaScript/TypeScript: import { name } from 'module'
r'^\s*import\s+\{([^}]+)\}\s+from\s+[\'"]([^"\']+)[\'"]',
# JavaScript/TypeScript: import name from 'module'
r'^\s*import\s+([a-zA-Z_][\w]*)\s+from\s+[\'"]([^"\']+)[\'"]',
# JavaScript: require('module')
r'\brequire\s*\(\s*[\'"]([^"\']+)[\'"]\s*\)',
# Go: import "path"
r'^\s*import\s+[\'"]([^"\']+)[\'"]',
# Go: import ( "path" )
r'^\s*[\'"]([^"\']+)[\'"]', # Inside import block
]
lines = self.source_content.splitlines()
in_import_block = False
for line in lines:
stripped = line.strip()
# Track Go-style import blocks
if stripped.startswith('import ('):
in_import_block = True
continue
if in_import_block and stripped == ')':
in_import_block = False
continue
for pattern in import_patterns:
match = re.search(pattern, line)
if match:
groups = match.groups()
if len(groups) >= 2:
# Pattern captured both name and source
names = groups[0]
source = groups[1]
# Handle multiple names (e.g., "name1, name2")
for name in re.split(r'[,\s]+', names):
name = name.strip().strip('*')
if name and name != 'as':
self.imported_symbols[name] = source
self.import_statements.append({
'name': name,
'source': source,
'line': line
})
elif len(groups) == 1:
# Pattern captured just module name
module = groups[0]
# Extract short name (last component)
short_name = module.split('.')[-1].split('/')[-1]
self.imported_symbols[short_name] = module
self.import_statements.append({
'name': short_name,
'source': module,
'line': line
})
break
log.debug('[GraphQL] Extracted ' + str(len(self.imported_symbols)) + ' imports from ' + self.path)
# =========================================================================
# PHASE 1: LIGHT PARSING
# =========================================================================
def light_parse(self):
"""
PHASE 1: Light parsing to extract global structures.
This method:
1. Reads the source file
2. Builds a coarse AST
3. Extracts global elements (classes, functions, etc.)
4. Creates CAST objects for discovered elements
Override _build_light_ast() and _extract_globals() for custom logic.
"""
log.debug('[GraphQL] Light parsing: ' + self.path)
# Read source content
self._read_source()
# Build coarse AST
self.ast = self._build_light_ast()
# Create program-level object
self._create_program_object()
# Extract global structures
self._extract_globals()
def _read_source(self):
"""Read the source file content."""
try:
with open(self.path, 'r', encoding='utf-8') as f:
self.source_content = f.read()
except UnicodeDecodeError:
# Fallback to latin-1 for non-UTF8 files
with open(self.path, 'r', encoding='latin-1') as f:
self.source_content = f.read()
except Exception as e:
log.warning('[GraphQL] Failed to read file: ' + str(e))
self.source_content = ''
def _get_structure_patterns(self):
"""
Get regex patterns for detecting global structures.
These patterns are configurable via config_input.json grammar section.
Multiple patterns per object type are supported.
Returns:
dict: {node_type: [list of compiled_regex_patterns]}
"""
# Patterns defined in config_input.json - multiple patterns per type supported
raw_patterns = {
'schema_def': ['^\\s*schema\\s*\\{', '^\\s*schema\\s*@[^{]*\\{'],
'type_def': ['^\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@[^{]*\\{', '^\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'type_def_implements': ['^\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+implements\\s+[A-Z][A-Za-z0-9_]*\\s*\\{'],
'type_def_implements_multiple': ['^\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+implements\\s+[A-Z][A-Za-z0-9_&\\s]*\\s*\\{'],
'type_extend': ['^\\s*extend\\s+type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*extend\\s+type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@[^{]*\\{', '^\\s*extend\\s+type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'type_extend_implements': ['^\\s*extend\\s+type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+implements\\s+[A-Z][A-Za-z0-9_&\\s]*\\s*\\{'],
'type_def_description': ['^\\s*"""[^"]*"""\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*"[^"]*"\\s*type\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{'],
'interface_def': ['^\\s*interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@[^{]*\\{', '^\\s*interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'interface_def_implements': ['^\\s*interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+implements\\s+[A-Z][A-Za-z0-9_&\\s]*\\s*\\{'],
'interface_extend': ['^\\s*extend\\s+interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*extend\\s+interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'interface_def_description': ['^\\s*"""[^"]*"""\\s*interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*"[^"]*"\\s*interface\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{'],
'enum_def': ['^\\s*enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@[^{]*\\{', '^\\s*enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'enum_extend': ['^\\s*extend\\s+enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*extend\\s+enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'enum_def_description': ['^\\s*"""[^"]*"""\\s*enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*"[^"]*"\\s*enum\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{'],
'enum_value': ['^\\s*(?P<n>[A-Z][A-Z0-9_]*)\\s*$', '^\\s*(?P<n>[A-Z][A-Z0-9_]*)\\s*(?:#|$)'],
'enum_value_deprecated': ['^\\s*(?P<n>[A-Z][A-Z0-9_]*)\\s*@deprecated'],
'enum_value_directive': ['^\\s*(?P<n>[A-Z][A-Z0-9_]*)\\s*@[a-zA-Z]'],
'enum_value_description': ['^\\s*"""[^"]*"""\\s*(?P<n>[A-Z][A-Z0-9_]*)', '^\\s*"[^"]*"\\s*(?P<n>[A-Z][A-Z0-9_]*)'],
'input_def': ['^\\s*input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@[^{]*\\{', '^\\s*input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'input_extend': ['^\\s*extend\\s+input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*extend\\s+input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'input_def_description': ['^\\s*"""[^"]*"""\\s*input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*"[^"]*"\\s*input\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{'],
'union_def': ['^\\s*union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*=', '^\\s*union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@[^=]*=', '^\\s*union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'union_extend': ['^\\s*extend\\s+union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*=', '^\\s*extend\\s+union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'union_def_description': ['^\\s*"""[^"]*"""\\s*union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*=', '^\\s*"[^"]*"\\s*union\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*='],
'scalar_def': ['^\\s*scalar\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$', '^\\s*scalar\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*(?:#|$)'],
'scalar_directive': ['^\\s*scalar\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*@'],
'scalar_def_description': ['^\\s*"""[^"]*"""\\s*scalar\\s+(?P<n>[A-Z][A-Za-z0-9_]*)', '^\\s*"[^"]*"\\s*scalar\\s+(?P<n>[A-Z][A-Za-z0-9_]*)'],
'directive_def': ['^\\s*directive\\s+@(?P<n>[a-z][A-Za-z0-9_]*)\\s+on\\s+'],
'directive_def_with_args': ['^\\s*directive\\s+@(?P<n>[a-z][A-Za-z0-9_]*)\\s*\\('],
'directive_def_repeatable': ['^\\s*directive\\s+@(?P<n>[a-z][A-Za-z0-9_]*)\\s*\\([^)]*\\)\\s*repeatable\\s+on'],
'field_def': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*:\\s*[A-Z\\[]', '^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*:\\s*[A-Z][A-Za-z0-9_]*\\s*(?:#|$)'],
'field_def_with_args': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*\\([^)]+\\)\\s*:\\s*'],
'field_def_non_null': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*(?:\\([^)]*\\))?\\s*:\\s*[A-Z\\[][^!]*!'],
'field_def_list': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*(?:\\([^)]*\\))?\\s*:\\s*\\[[A-Z][A-Za-z0-9_!]*\\]'],
'field_def_list_non_null': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*(?:\\([^)]*\\))?\\s*:\\s*\\[[A-Z][A-Za-z0-9_!]*\\]!'],
'field_def_deprecated': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*[:(][^@]*@deprecated'],
'field_def_directive': ['^\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*[:(][^@]*@[a-z]'],
'field_def_description': ['^\\s*"""[^"]*"""\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*:', '^\\s*"[^"]*"\\s*(?P<n>[a-z][A-Za-z0-9_]*)\\s*:'],
'query_operation': ['^\\s*(?P<n>query)\\s*\\{'],
'query_operation_named': ['^\\s*query\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*query\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'query_operation_with_vars': ['^\\s*query\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\([^)]*\\)\\s*\\{', '^\\s*query\\s*\\([^)]*\\)\\s*\\{'],
'query_operation_anonymous': ['^\\s*\\{\\s*[a-z][A-Za-z0-9_]*'],
'mutation_operation': ['^\\s*(?P<n>mutation)\\s*\\{'],
'mutation_operation_named': ['^\\s*mutation\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*mutation\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'mutation_operation_with_vars': ['^\\s*mutation\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\([^)]*\\)\\s*\\{', '^\\s*mutation\\s*\\([^)]*\\)\\s*\\{'],
'subscription_operation': ['^\\s*(?P<n>subscription)\\s*\\{'],
'subscription_operation_named': ['^\\s*subscription\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{', '^\\s*subscription\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*$'],
'subscription_operation_with_vars': ['^\\s*subscription\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\([^)]*\\)\\s*\\{', '^\\s*subscription\\s*\\([^)]*\\)\\s*\\{'],
'fragment_def': ['^\\s*fragment\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+on\\s+[A-Z][A-Za-z0-9_]*\\s*\\{', '^\\s*fragment\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+on\\s+[A-Z][A-Za-z0-9_]*\\s*$'],
'fragment_def_directive': ['^\\s*fragment\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s+on\\s+[A-Z][A-Za-z0-9_]*\\s*@[a-z][^{]*\\{'],
'inline_fragment': ['^\\s*\\.\\.\\.\\s*on\\s+(?P<n>[A-Z][A-Za-z0-9_]*)\\s*\\{'],
'variable_def': ['\\$(?P<n>[a-z][A-Za-z0-9_]*)\\s*:\\s*[A-Z\\[][A-Za-z0-9_\\[\\]!]*'],
'variable_def_default': ['\\$(?P<n>[a-z][A-Za-z0-9_]*)\\s*:\\s*[A-Z\\[][A-Za-z0-9_\\[\\]!]*\\s*='],
'variable_def_non_null': ['\\$(?P<n>[a-z][A-Za-z0-9_]*)\\s*:\\s*[A-Z\\[][A-Za-z0-9_\\[\\]]*!'],
}
patterns = {}
for obj_type, pattern_list in raw_patterns.items():
compiled = []
for p in pattern_list:
try:
compiled.append(re.compile(p, re.IGNORECASE))
except Exception as e:
log.warning('[GraphQL] Invalid pattern for ' + obj_type + ': ' + str(e))
if compiled:
patterns[obj_type] = compiled
return patterns
def _get_container_types(self):
"""
Get object types that can contain other objects.
Returns:
set: Object types that have children (based on parent relationships)
"""
container_types = set()
for obj_type, obj_def in OBJECTS_CONFIG.items():
parent = obj_def.get('parent', 'file')
if parent != 'file' and parent != 'Program':
# This parent type is a container
container_types.add(parent)
return container_types
def _get_children_types(self, parent_type):
"""
Get object types that can be children of a given parent type.
Args:
parent_type (str): Parent object type name
Returns:
set: Pattern keys for objects that are children of this type
"""
child_patterns = set()
for obj_type, obj_def in OBJECTS_CONFIG.items():
if obj_def.get('parent') == parent_type:
for pattern_key in obj_def.get('pattern_keys', []):
child_patterns.add(pattern_key)
return child_patterns
def _get_block_delimiter_style(self):
"""
Get the block delimiter style for this language.
Returns:
str: 'braces', 'end_keyword', or 'indentation'
"""
return 'braces'
def _build_light_ast(self):
"""
Build a coarse AST from the source content.
Dynamically handles object hierarchy based on OBJECTS_CONFIG.
Supports multiple block delimiter styles:
- braces: {...} (Go, Java, Rust, C, etc.)
- end_keyword: def...end (Ruby, Lua)
- indentation: Python-style
- sequential: Block ends when next block starts (COBOL, Assembly, MUMPS)
Returns:
ASTNode: Root node of the AST
"""
root = ASTNode('module', self.get_base_name())
root.start_line = 1
root.end_line = len(self.source_content.splitlines()) if self.source_content else 1
if not self.source_content:
return root
lines = self.source_content.splitlines()
# Get patterns (multiple per type)
patterns = self._get_structure_patterns()
# Determine which pattern types are containers (can have children)
container_patterns = set()
for obj_type in self._get_container_types():
for pattern_key in OBJECTS_CONFIG.get(obj_type, {}).get('pattern_keys', []):
container_patterns.add(pattern_key)
# ALL pattern types need to be tracked for end_line calculation
all_tracked_patterns = set(patterns.keys())
# Determine block delimiter style
block_style = self._get_block_delimiter_style()
# Stack of current containers: [(node, depth_marker)]
# For braces: depth_marker = brace_depth at start
# For end_keyword: depth_marker = keyword_depth
# For indentation: depth_marker = indentation_level
# For sequential: depth_marker = pattern_key (to match same-level blocks)
container_stack = []
depth = 0
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Calculate depth change based on block style
if block_style == 'braces':
open_count = line.count('{')
close_count = line.count('}')
new_depth = depth + open_count - close_count
# Check if any containers have ended
while container_stack and new_depth <= container_stack[-1][1]:
ended_node, _ = container_stack.pop()
ended_node.end_line = i
depth = new_depth
depth_at_match = depth - open_count # Depth before the open brace
should_track = open_count > 0
# Flag for single-line blocks (e.g., "int func() { return 0; }")
# This will be used to close the block immediately after adding it
single_line_block = (open_count > 0 and close_count > 0 and open_count == close_count)
elif block_style == 'end_keyword':
# Count opening keywords (class, module, def, do, if, etc.)
# and closing keywords (end)
open_keywords = 0
close_keywords = 0
# Opening patterns - be careful with word boundaries
# Includes both Ruby (class, module, def) and Lua (function) keywords
if re.match(r'^\s*(class|module|def|do|if|unless|case|while|until|for|begin|function)\b', line):
open_keywords = 1
# Handle block openers like { or do
if re.search(r'\bdo\s*(\|[^|]*\|)?\s*$', line):
open_keywords = 1
# Closing keyword
if re.match(r'^\s*end\b', stripped):
close_keywords = 1
new_depth = depth + open_keywords - close_keywords
# Check if any containers have ended (when we see 'end')
if close_keywords > 0 and container_stack:
ended_node, _ = container_stack.pop()
ended_node.end_line = i
depth = new_depth
depth_at_match = depth - open_keywords
should_track = open_keywords > 0
single_line_block = False # Not applicable for end_keyword style
elif block_style == 'indentation':
# Calculate indentation level
if stripped: # Non-empty line
indent = len(line) - len(line.lstrip())
new_depth = indent
# Check if any containers have ended (decreased indentation)
while container_stack and new_depth <= container_stack[-1][1] and stripped:
ended_node, _ = container_stack.pop()
ended_node.end_line = i - 1 # Previous line ends the block
depth = new_depth
else:
new_depth = depth # Keep current depth for empty lines
depth_at_match = depth
should_track = True # Always track in indentation mode
single_line_block = False # Not applicable for indentation style
elif block_style == 'sequential':
# Sequential mode: blocks end when the next block at same level starts
# Used for COBOL paragraphs, Assembly labels, MUMPS routines, etc.
# We'll check for matches first, then close previous blocks
depth_at_match = 0
should_track = True # Always track in sequential mode
single_line_block = False # Not applicable for sequential style
# Note: block closing happens AFTER pattern matching below
else:
# Default to braces behavior
depth_at_match = 0
should_track = True
single_line_block = False
# Now check for pattern matches
matched_on_line = False
for pattern_key, pattern_list in patterns.items():
if matched_on_line:
break
for pattern in pattern_list:
match = pattern.match(line)
if match:
# Support both (?P<name>...) and (?P<n>...) named groups
groups = match.groupdict()
if 'name' in groups:
name = match.group('name')
elif 'n' in groups:
name = match.group('n')
else:
name = match.group(1) if match.groups() else None
if not name:
continue
node = ASTNode(pattern_key, name, start_line=i)
# Check for receiver/parent group in pattern
if 'receiver' in match.groupdict() and match.group('receiver'):
receiver = match.group('receiver').lstrip('*')
node.properties['receiver'] = receiver
# Determine where to add this node based on hierarchy
obj_type = PATTERN_TO_OBJECT_TYPE.get(pattern_key)
parent_type = OBJECT_PARENTS.get(obj_type, 'Program') if obj_type else 'Program'
# SEQUENTIAL MODE: Close previous block of same or higher level
# before adding the new one
if block_style == 'sequential' and container_stack:
# Close all blocks at same level or lower in hierarchy
# A new block at same level closes the previous one
while container_stack:
prev_node, prev_pattern = container_stack[-1]
prev_obj_type = PATTERN_TO_OBJECT_TYPE.get(prev_node.type)
prev_parent = OBJECT_PARENTS.get(prev_obj_type, 'Program') if prev_obj_type else 'Program'
# If previous block is at same level (same parent), close it
# Also close if new block is at a higher level
if prev_parent == parent_type or parent_type in ('Program', 'file'):
ended = container_stack.pop()
ended[0].end_line = i - 1
else:
break
# Find the appropriate parent in the stack
added = False
if parent_type != 'Program' and parent_type != 'file':
for container_node, _ in reversed(container_stack):
container_obj_type = PATTERN_TO_OBJECT_TYPE.get(container_node.type)
if container_obj_type == parent_type:
container_node.add_child(node)
added = True
break
if not added and container_stack:
nearest_container, _ = container_stack[-1]
nearest_container.add_child(node)
added = True
if not added:
root.add_child(node)
# Track this construct for end_line calculation
if pattern_key in all_tracked_patterns and should_track:
# For sequential mode, store pattern_key as depth_marker
if block_style == 'sequential':
container_stack.append((node, pattern_key))
else:
container_stack.append((node, depth_at_match))
# Close immediately if this is a single-line block
if block_style == 'braces' and 'single_line_block' in locals() and single_line_block:
node.end_line = i
container_stack.pop()
matched_on_line = True
break
# Close any remaining open containers at end of file
while container_stack:
ended_node, _ = container_stack.pop()
if ended_node.end_line == 0:
ended_node.end_line = len(lines)
return root
def _create_program_object(self):
"""Create the program-level container object."""
self.program = CustomObject()
self.program.set_type('GraphQLProgram')
self.program.set_name(self.get_filename())
self.program.set_fullname(self.path)