|
13 | 13 | Python ``def`` / ``async def`` / ``class`` at module level, plus methods |
14 | 14 | one indent inside a class (recorded with their enclosing class). |
15 | 15 | JS / TS ``function`` / ``async function`` / ``export function`` / ``class``, |
16 | | - and ``const name = (...) =>`` / ``const name = function`` style |
17 | | - assignments. |
| 16 | + ``const name = (...) =>`` / ``const name = function`` assignments, and |
| 17 | + methods one indent inside a class (recorded with their class). |
18 | 18 |
|
19 | 19 | Each entry carries the name, its kind (function / class / method), the |
20 | 20 | enclosing class for a method, the file, and a 1-based line number. |
@@ -140,28 +140,67 @@ class is a method; a ``def`` nested inside another ``def`` is a local helper |
140 | 140 | r"^\s*(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+(" + _IDENT + r")\s*=\s*" |
141 | 141 | r"(?:async\s+)?(?:function\b|\*?\s*\([^)]*\)\s*=>|" + _IDENT + r"\s*=>)" |
142 | 142 | ) |
| 143 | +# A method *declaration* inside a class body: optional TS modifiers, an optional |
| 144 | +# get/set or generator marker, the name, a parameter list, an optional TS return |
| 145 | +# annotation, and the opening brace on the same line. The trailing ``{`` is what |
| 146 | +# separates a declaration (``render() {``) from a call (``render();``). |
| 147 | +_JS_METHOD_RE = re.compile( |
| 148 | + r"^\s*(?:(?:public|private|protected|readonly|static|async|override|abstract)\s+)*" |
| 149 | + r"(?:(?:get|set)\s+)?\*?\s*" |
| 150 | + r"(?P<name>" + _IDENT + r")\s*\([^;]*\)\s*(?::[^={};]+)?\{" |
| 151 | +) |
| 152 | +# Control-flow heads also read as ``word (...) {``; never treat them as methods. |
| 153 | +_JS_NON_METHODS = frozenset( |
| 154 | + {"if", "for", "while", "switch", "catch", "do", "with", "return", "function", "else"} |
| 155 | +) |
143 | 156 |
|
144 | 157 |
|
145 | 158 | def _js_definitions(path: str, content: str) -> list[dict]: |
146 | 159 | out: list[dict] = [] |
| 160 | + # frames: list of (indent, kind, name) for currently open class/block scopes, |
| 161 | + # mirroring the Python pass so a method is only read inside a class body and a |
| 162 | + # method's own body is not re-scanned for nested "methods". |
| 163 | + frames: list[tuple[int, str, str]] = [] |
| 164 | + |
147 | 165 | for lineno, raw in enumerate(content.splitlines(), start=1): |
148 | 166 | stripped = raw.strip() |
149 | 167 | if not stripped or stripped.startswith("//"): |
150 | 168 | continue |
151 | 169 |
|
| 170 | + indent = _indent_width(raw) |
| 171 | + while frames and frames[-1][0] >= indent: |
| 172 | + frames.pop() |
| 173 | + enclosing = frames[-1] if frames else None |
| 174 | + |
152 | 175 | class_match = _JS_CLASS_RE.match(raw) |
153 | 176 | if class_match: |
154 | | - out.append(_entry(path, "js", class_match.group(1), "class", None, lineno)) |
| 177 | + name = class_match.group(1) |
| 178 | + parent = enclosing[2] if enclosing and enclosing[1] == "class" else None |
| 179 | + out.append(_entry(path, "js", name, "class", parent, lineno)) |
| 180 | + frames.append((indent, "class", name)) |
155 | 181 | continue |
156 | 182 |
|
157 | 183 | func_match = _JS_FUNC_RE.match(raw) |
158 | 184 | if func_match: |
159 | | - out.append(_entry(path, "js", func_match.group(1), "function", None, lineno)) |
| 185 | + name = func_match.group(1) |
| 186 | + out.append(_entry(path, "js", name, "function", None, lineno)) |
| 187 | + frames.append((indent, "block", name)) |
160 | 188 | continue |
161 | 189 |
|
162 | 190 | assign_match = _JS_ASSIGN_RE.match(raw) |
163 | 191 | if assign_match: |
164 | | - out.append(_entry(path, "js", assign_match.group(1), "function", None, lineno)) |
| 192 | + name = assign_match.group(1) |
| 193 | + out.append(_entry(path, "js", name, "function", None, lineno)) |
| 194 | + frames.append((indent, "block", name)) |
| 195 | + continue |
| 196 | + |
| 197 | + if enclosing is not None and enclosing[1] == "class": |
| 198 | + method_match = _JS_METHOD_RE.match(raw) |
| 199 | + if method_match: |
| 200 | + name = method_match.group("name") |
| 201 | + if name not in _JS_NON_METHODS: |
| 202 | + out.append(_entry(path, "js", name, "method", enclosing[2], lineno)) |
| 203 | + frames.append((indent, "block", name)) |
165 | 204 |
|
166 | 205 | return out |
167 | 206 |
|
@@ -437,8 +476,9 @@ def _outline_notes(path: str, lang: str, defs: list[dict], total: int) -> list[s |
437 | 476 | notes = [f"{name} defines {', '.join(parts)}, listed in the order they appear."] |
438 | 477 | if lang == "js": |
439 | 478 | notes.append( |
440 | | - "For JS/TS, methods inside a class are not broken out yet; the class " |
441 | | - "is listed as a whole." |
| 479 | + "For JS/TS, class methods are broken out when the class body is " |
| 480 | + "conventionally indented; arrow-function fields and computed names " |
| 481 | + "are still listed under the class as a whole." |
442 | 482 | ) |
443 | 483 | notes.append("Regex-based outline: runtime-built or re-exported names may be missed.") |
444 | 484 | return notes |
0 commit comments