10
10
from functools import cache
11
11
from inspect import isclass
12
12
from json import load
13
+ from operator import attrgetter
13
14
from pathlib import Path
14
15
from tempfile import NamedTemporaryFile
15
16
from typing import Annotated , Any , Literal , TypedDict
@@ -103,6 +104,16 @@ def parse(json: dict[str, Any]) -> "Project":
103
104
PostConvertType = typing .Callable [["Converter" , "Node | Signature" , ir .TopLevel ], None ]
104
105
105
106
107
+ def _parse_filepath (path : str , base_dir : str ) -> list [str ]:
108
+ p = Path (path ).resolve ().relative_to (base_dir )
109
+ if p .name :
110
+ p = p .with_suffix ("" )
111
+ entries = ["." ] + list (p .parts )
112
+ for i in range (len (entries ) - 1 ):
113
+ entries [i ] += "/"
114
+ return entries
115
+
116
+
106
117
class Converter :
107
118
base_dir : str
108
119
index : dict [int , "IndexType" ]
@@ -134,15 +145,6 @@ def populate_index(self, root: "Project") -> "Converter":
134
145
self ._populate_index_inner (root , parent = None , idmap = root .symbolIdMap )
135
146
return self
136
147
137
- def _parse_filepath (self , path : str ) -> list [str ]:
138
- p = Path (path ).resolve ().relative_to (self .base_dir )
139
- if p .name :
140
- p = p .with_suffix ("" )
141
- entries = ["." ] + list (p .parts )
142
- for i in range (len (entries ) - 1 ):
143
- entries [i ] += "/"
144
- return entries
145
-
146
148
def _populate_index_inner (
147
149
self ,
148
150
node : "IndexType" ,
@@ -156,7 +158,9 @@ def _populate_index_inner(
156
158
parent_kind = parent .kindString if parent else ""
157
159
parent_segments = parent .path if parent else []
158
160
if str (node .id ) in idmap :
159
- filepath = self ._parse_filepath (idmap [str (node .id )].sourceFileName )
161
+ filepath = _parse_filepath (
162
+ idmap [str (node .id )].sourceFileName , self .base_dir
163
+ )
160
164
if filepath :
161
165
node .filepath = filepath
162
166
self .compute_path (node , parent_kind , parent_segments , filepath )
@@ -208,9 +212,12 @@ def compute_path(
208
212
209
213
node .path = segments
210
214
211
- def convert_all_nodes (self , root : "Project" ) -> list [ir .TopLevel ]:
215
+ def convert_all_nodes (
216
+ self , root : "Project"
217
+ ) -> tuple [list [ir .TopLevel ], list [ir .TopLevel ]]:
212
218
todo : list [Node | Signature ] = list (root .children )
213
219
done = []
220
+ top_level = []
214
221
while todo :
215
222
node = todo .pop ()
216
223
if node .sources and node .sources [0 ].fileName [0 ] == "/" :
@@ -221,13 +228,17 @@ def convert_all_nodes(self, root: "Project") -> list[ir.TopLevel]:
221
228
if converted :
222
229
self ._post_convert (self , node , converted )
223
230
done .append (converted )
224
- return done
231
+ if converted and getattr (node , "top_level" , False ):
232
+ top_level .append (converted )
233
+ return done , top_level
225
234
226
235
227
236
class Analyzer :
237
+ modules : dict [str , ir .Module ]
238
+
228
239
def __init__ (
229
240
self ,
230
- json : "Project" ,
241
+ project : "Project" ,
231
242
base_dir : str ,
232
243
* ,
233
244
should_destructure_arg : ShouldDestructureArgType | None = None ,
@@ -243,12 +254,21 @@ def __init__(
243
254
base_dir ,
244
255
should_destructure_arg = should_destructure_arg ,
245
256
post_convert = post_convert ,
246
- ).populate_index (json )
247
- ir_objects = converter .convert_all_nodes (json )
257
+ ).populate_index (project )
258
+ for child in project .children :
259
+ child .top_level = True
260
+ if isinstance (child , Module ):
261
+ for c in child .children :
262
+ c .top_level = True
263
+
264
+ ir_objects , top_level = converter .convert_all_nodes (project )
248
265
249
266
self ._base_dir = base_dir
250
267
self ._objects_by_path : SuffixTree [ir .TopLevel ] = SuffixTree ()
251
268
self ._objects_by_path .add_many ((obj .path .segments , obj ) for obj in ir_objects )
269
+ modules = self ._create_modules (top_level )
270
+ self ._modules_by_path : SuffixTree [ir .Module ] = SuffixTree ()
271
+ self ._modules_by_path .add_many ((obj .path .segments , obj ) for obj in modules )
252
272
253
273
@classmethod
254
274
def from_disk (
@@ -272,19 +292,41 @@ def get_object(
272
292
"""Return the IR object with the given path suffix.
273
293
274
294
:arg as_type: Ignored
275
-
276
- We can't scan through the raw TypeDoc output at runtime like the JSDoc
277
- analyzer does, because it's just a linear list of files, each
278
- containing a nested tree of nodes. They're not indexed at all. And
279
- since we need to index by suffix, we need to traverse all the way down,
280
- eagerly. Also, we will keep the flattening, because we need it to
281
- resolve the IDs of references. (Some of the references are potentially
282
- important in the future: that's how TypeDoc points to superclass
283
- definitions of methods inherited by subclasses.)
284
-
285
295
"""
286
296
return self ._objects_by_path .get (path_suffix )
287
297
298
+ def _create_modules (self , ir_objects : list [ir .TopLevel ]) -> Iterable [ir .Module ]:
299
+ """Search through the doclets generated by JsDoc and categorize them by
300
+ summary section. Skip docs labeled as "@private".
301
+ """
302
+ modules = {}
303
+ for obj in ir_objects :
304
+ assert obj .deppath
305
+ path = obj .deppath .split ("/" )
306
+ for i in range (len (path ) - 1 ):
307
+ path [i ] += "/"
308
+ if obj .deppath not in modules :
309
+ modules [obj .deppath ] = ir .Module (
310
+ filename = obj .deppath , path = ir .Pathname (path ), line = 1
311
+ )
312
+ mod = modules [obj .deppath ]
313
+ if "attribute" in obj .modifier_tags :
314
+ mod .attributes .append (obj )
315
+ continue
316
+ match obj :
317
+ case ir .Attribute (_):
318
+ mod .attributes .append (obj )
319
+ case ir .Function (_):
320
+ mod .functions .append (obj )
321
+ case ir .Class (_):
322
+ mod .classes .append (obj )
323
+
324
+ for mod in modules .values ():
325
+ mod .attributes = sorted (mod .attributes , key = attrgetter ("name" ))
326
+ mod .functions = sorted (mod .functions , key = attrgetter ("name" ))
327
+ mod .classes = sorted (mod .classes , key = attrgetter ("name" ))
328
+ return modules .values ()
329
+
288
330
289
331
class Source (BaseModel ):
290
332
fileName : str
@@ -410,6 +452,7 @@ class TopLevelProperties(Base):
410
452
name : str
411
453
kindString : str
412
454
comment_ : Comment = Field (default_factory = Comment , alias = "comment" )
455
+ top_level : bool = False
413
456
414
457
@property
415
458
def comment (self ) -> Comment :
@@ -976,11 +1019,12 @@ def inner(param: Param) -> Iterator[str | ir.TypeXRef]:
976
1019
def to_ir (
977
1020
self , converter : Converter
978
1021
) -> tuple [ir .Function | None , Sequence ["Node" ]]:
979
- if self .name .startswith ("[" ):
1022
+ SYMBOL_PREFIX = "[Symbol\u2024 "
1023
+ if self .name .startswith ("[" ) and not self .name .startswith (SYMBOL_PREFIX ):
980
1024
# a symbol.
981
1025
# \u2024 looks like a period but is not a period.
982
1026
# This isn't ideal, but otherwise the coloring is weird.
983
- self .name = "[Symbol \u2024 " + self .name [1 :]
1027
+ self .name = SYMBOL_PREFIX + self .name [1 :]
984
1028
self ._fix_type_suffix ()
985
1029
params = self ._destructure_params (converter )
986
1030
# Would be nice if we could statically determine that the function was
0 commit comments