mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
Merge branch 'main' into feat/sofa-framework-extractor
This commit is contained in:
commit
718b80336c
5 changed files with 266 additions and 39 deletions
|
|
@ -1 +1 @@
|
|||
9803b81f0c3738ecd276aba187436482be47b5f5f62e5e85a983524129713b7d
|
||||
06687dff942d531c4d453b5906a8666c90db4867eb43ed18304aa59a8a93ef9d
|
||||
|
|
|
|||
|
|
@ -13,11 +13,35 @@ const PYTHON_SCOPE_QUERY = `
|
|||
(module) @scope.module
|
||||
(class_definition) @scope.class
|
||||
(function_definition) @scope.function
|
||||
(lambda) @scope.function
|
||||
|
||||
;; Declarations
|
||||
(class_definition
|
||||
name: (identifier) @declaration.name) @declaration.class
|
||||
|
||||
;; Heritage — bare identifier
|
||||
;; NOTE: captures.ts on main already synthesizes @reference.inherits for
|
||||
;; qualified bases via #1951/#1956. These @heritage.* patterns are redundant
|
||||
;; with that synthesis but kept as documentation and a safety net for the
|
||||
;; generic heritage extractor path. They produce topicOf edges that the
|
||||
;; resolution pipeline ignores when the synthesis path wins.
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclasses: (argument_list
|
||||
(identifier) @heritage.extends)) @heritage
|
||||
|
||||
;; Heritage — qualified base (module.Class)
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclasses: (argument_list
|
||||
(attribute) @heritage.extends)) @heritage
|
||||
|
||||
;; Heritage — subscripted/generic base (Generic[T])
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclasses: (argument_list
|
||||
(subscript) @heritage.extends)) @heritage
|
||||
|
||||
(function_definition
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
|
|
@ -234,6 +258,22 @@ const PYTHON_SCOPE_QUERY = `
|
|||
name: (identifier) @type-binding.name
|
||||
return_type: (type) @type-binding.type) @type-binding.return
|
||||
|
||||
;; Decorators — simple @decorator
|
||||
(decorator
|
||||
(identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; Decorators — @obj.decorator (single attribute, identifier receiver)
|
||||
(decorator
|
||||
(attribute
|
||||
object: (identifier) @reference.receiver
|
||||
attribute: (identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; Decorators — @a.b.decorator (nested attributes)
|
||||
(decorator
|
||||
(attribute
|
||||
object: (attribute) @reference.receiver
|
||||
attribute: (identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; References — calls
|
||||
(call
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
|
|
|||
19
gitnexus/test/fixtures/lang-resolution/python-parsing-coverage/heritage.py
vendored
Normal file
19
gitnexus/test/fixtures/lang-resolution/python-parsing-coverage/heritage.py
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Heritage fixture — bare, qualified, and subscripted bases."""
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class BaseModel:
|
||||
pass
|
||||
|
||||
class Bare(BaseModel):
|
||||
pass
|
||||
|
||||
class Qualified(mod.BaseModel):
|
||||
pass
|
||||
|
||||
class Subscripted(Generic[T]):
|
||||
pass
|
||||
|
||||
class Both(mod.BaseModel, Generic[T]):
|
||||
pass
|
||||
|
|
@ -4,12 +4,12 @@
|
|||
"digest": "8662c17b0f21fcfa650065abd62f0c9b7e1c65bf8a1f7dce6f2a16bba9df759f"
|
||||
},
|
||||
"python-abstract-dispatch/base.py": {
|
||||
"captureGroups": 16,
|
||||
"digest": "2d25dcc17cb5b31cea26c15776d3a8cae290790d92a7adc6d6155be534fdd75b"
|
||||
"captureGroups": 19,
|
||||
"digest": "892a2e6ad60f7fc6e206bcc42f9c8905dfbab27e84b4334a3a4ba353078a879a"
|
||||
},
|
||||
"python-abstract-dispatch/impl.py": {
|
||||
"captureGroups": 15,
|
||||
"digest": "6c27015f13d32024ce06c1515ab29ca0436df7a4864bbb62188d5f79d685cc31"
|
||||
"captureGroups": 16,
|
||||
"digest": "5f785934a573d11499ccea29ae87daaaf817390884acd1328a803ec0ce828297"
|
||||
},
|
||||
"python-alias-imports/app.py": {
|
||||
"captureGroups": 13,
|
||||
|
|
@ -40,8 +40,8 @@
|
|||
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"python-ambiguous/services/user_handler.py": {
|
||||
"captureGroups": 8,
|
||||
"digest": "5a4bb82d0e6a6a53fe012f197e42c572ac1169ff38e84db9c6281399f6739021"
|
||||
"captureGroups": 9,
|
||||
"digest": "b0ee065813f8d113ad5f6cc413a36bc645e05fe9b533c3a40cd0b62fe9eb77ca"
|
||||
},
|
||||
"python-ancestor-import/a/b/c/deep.py": {
|
||||
"captureGroups": 5,
|
||||
|
|
@ -128,8 +128,8 @@
|
|||
"digest": "0f60d5cd521b0073524b0993e82d5291f86badd5cbefb986cefdf7b0bed64157"
|
||||
},
|
||||
"python-child-extends-parent/child.py": {
|
||||
"captureGroups": 5,
|
||||
"digest": "d118691eb76c9432841743efee8556f1e7a1d136e9b403a12fd512f91d73ca61"
|
||||
"captureGroups": 6,
|
||||
"digest": "48c1f798021986fbe074bb37fdde4763791bfa9a18d1172510a7f4fe4ed676d5"
|
||||
},
|
||||
"python-child-extends-parent/parent.py": {
|
||||
"captureGroups": 7,
|
||||
|
|
@ -208,16 +208,16 @@
|
|||
"digest": "392b15be747e2b5cbd3ac5a9e61a7677ffa6ba52e49d3631681e43c557373b5f"
|
||||
},
|
||||
"python-django-app-imports/accounts/apps.py": {
|
||||
"captureGroups": 6,
|
||||
"digest": "784cba903ad9534337ed820b085c8ecc352964e797bde1d4dda9e700960366a0"
|
||||
"captureGroups": 7,
|
||||
"digest": "d7ef23ddaa13aa398f642580fd19bfff9207e88d0015aeb23a04bbd3140c7bb4"
|
||||
},
|
||||
"python-django-app-imports/accounts/migrations/__init__.py": {
|
||||
"captureGroups": 0,
|
||||
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"python-django-app-imports/accounts/models.py": {
|
||||
"captureGroups": 8,
|
||||
"digest": "b240f4ea2135824ee47fd5f2d9a4788ff0374cafb5070b295fc710a523f19873"
|
||||
"captureGroups": 9,
|
||||
"digest": "a513607782f1a3d33af594aff2674ff3fa72e38588bf3590e9581e8ca0dcf47a"
|
||||
},
|
||||
"python-django-app-imports/accounts/tests.py": {
|
||||
"captureGroups": 2,
|
||||
|
|
@ -236,16 +236,16 @@
|
|||
"digest": "392b15be747e2b5cbd3ac5a9e61a7677ffa6ba52e49d3631681e43c557373b5f"
|
||||
},
|
||||
"python-django-app-imports/billing/apps.py": {
|
||||
"captureGroups": 6,
|
||||
"digest": "0ff487476397cc85c2ce5ec0afe59eb82d83f97bcb3d4518b5040f52790e1833"
|
||||
"captureGroups": 7,
|
||||
"digest": "64644bea82f36aef4f7f2784405a630b6b5f5b87f32f837372190642b8af3557"
|
||||
},
|
||||
"python-django-app-imports/billing/migrations/__init__.py": {
|
||||
"captureGroups": 0,
|
||||
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"python-django-app-imports/billing/models.py": {
|
||||
"captureGroups": 12,
|
||||
"digest": "eedb2e1992f85a6743555947118784db9c78f3c113d2d598e53dd54cb80a0629"
|
||||
"captureGroups": 13,
|
||||
"digest": "94396c0755b2fab7ef73c06f92a237893097b57bf83637b83d39f6d0e0f6e0d9"
|
||||
},
|
||||
"python-django-app-imports/billing/tests.py": {
|
||||
"captureGroups": 2,
|
||||
|
|
@ -348,12 +348,12 @@
|
|||
"digest": "5879a42d6655248623c9aee193fedcae51daa1887d8e597fdd944ad93af053a4"
|
||||
},
|
||||
"python-grandparent-resolution/models/b.py": {
|
||||
"captureGroups": 5,
|
||||
"digest": "be5c28ebfb06cd90c7cd453d612f0cacdffdac3e57d7ce794c0a353a9b057597"
|
||||
"captureGroups": 6,
|
||||
"digest": "58660d2049990cebed4ea917b45eb32d21120ee88633c97fa19d91277b25cd6a"
|
||||
},
|
||||
"python-grandparent-resolution/models/c.py": {
|
||||
"captureGroups": 5,
|
||||
"digest": "2ad9124422ca018e59855c55a054d5c37d15b448a90027842fc56dd6f61e4593"
|
||||
"captureGroups": 6,
|
||||
"digest": "a04777b65ef018c652b96aa8978a67d14ee94905a5d3328c00a4c83ed1fb486a"
|
||||
},
|
||||
"python-grandparent-resolution/models/greeting.py": {
|
||||
"captureGroups": 7,
|
||||
|
|
@ -472,8 +472,8 @@
|
|||
"digest": "88dfd417951b8083184f83da8c1e0c2b19700cfbf1f1ff203a904ebc00f50b30"
|
||||
},
|
||||
"python-method-enrichment/models.py": {
|
||||
"captureGroups": 25,
|
||||
"digest": "14c45aebe0fc6ad1a9328bda3da8cdf7eaa6b9d84641cd2d63c1a105aa18cd42"
|
||||
"captureGroups": 29,
|
||||
"digest": "83ebd37c527396004fdb9175beeeb09ed03645a04bc6a4eaa25b1260ac123ad1"
|
||||
},
|
||||
"python-module-export-vs-method-collision/app.py": {
|
||||
"captureGroups": 14,
|
||||
|
|
@ -500,16 +500,16 @@
|
|||
"digest": "98bcec072e85a50303be141212b835322f5f9e53f7fe5d77b23d8ee524623e84"
|
||||
},
|
||||
"python-multi-level-mro/child.py": {
|
||||
"captureGroups": 5,
|
||||
"digest": "d118691eb76c9432841743efee8556f1e7a1d136e9b403a12fd512f91d73ca61"
|
||||
"captureGroups": 6,
|
||||
"digest": "48c1f798021986fbe074bb37fdde4763791bfa9a18d1172510a7f4fe4ed676d5"
|
||||
},
|
||||
"python-multi-level-mro/grandparent.py": {
|
||||
"captureGroups": 7,
|
||||
"digest": "f9f81d3a37c55b3e23bec3774405920afa29c5793c46860a98a06c5d0c7f0980"
|
||||
},
|
||||
"python-multi-level-mro/parent.py": {
|
||||
"captureGroups": 5,
|
||||
"digest": "b68bfb8fdedb8f725c609264e604ccb674a5a775c0d87c008a9990234c70bde3"
|
||||
"captureGroups": 6,
|
||||
"digest": "884be03e3640693bc087e297d018ce37d92cfd7cc5742cbca61471cd4a3c9e8c"
|
||||
},
|
||||
"python-multi-segment-ancestor-import/backend/auth_utils.py": {
|
||||
"captureGroups": 6,
|
||||
|
|
@ -592,16 +592,20 @@
|
|||
"digest": "f0384bd6ecb7d1a9ad2306358917b7295c71ea8f1bcea818fd39c56d2c28c7e9"
|
||||
},
|
||||
"python-parent-resolution/models/user.py": {
|
||||
"captureGroups": 9,
|
||||
"digest": "b06a66a108097eec9427a028dec38bbab284918ac39344e86e58bba89533c530"
|
||||
"captureGroups": 10,
|
||||
"digest": "897b68f06f59fd0a488d0ac5d694aa655a82c09f8846202697a61def1f32c488"
|
||||
},
|
||||
"python-parsing-coverage/heritage.py": {
|
||||
"captureGroups": 26,
|
||||
"digest": "3ceaf5293361ca76d52aa6941bd82674a16d1bd002a1fa8bf0b2f84cd00120c2"
|
||||
},
|
||||
"python-pkg/models/base.py": {
|
||||
"captureGroups": 9,
|
||||
"digest": "4984ee01b7a9fefe622195f0e4925823c0e62a1714ca2dda5cd8250e5e45fa7c"
|
||||
},
|
||||
"python-pkg/models/user.py": {
|
||||
"captureGroups": 8,
|
||||
"digest": "9ee707b36f42a635fdb867ce20359b5f51ac4e13550cde801359dc8314e01a77"
|
||||
"captureGroups": 9,
|
||||
"digest": "7b6984be334f50334e6ebe4a895cf61e2d3773edd1c03ed41440b76abcd44f01"
|
||||
},
|
||||
"python-pkg/services/auth.py": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -636,8 +640,8 @@
|
|||
"digest": "7fc34dae23f54cdee030a2d4a1d80c0bf226ea3b37335ecf48551c4707d32e20"
|
||||
},
|
||||
"python-qualified-base/service.py": {
|
||||
"captureGroups": 16,
|
||||
"digest": "adecbd613fe97656cd5797c4dbef9f3c32bcd1b9faa5e765227af5d2001da427"
|
||||
"captureGroups": 20,
|
||||
"digest": "d1f66f8587c7c0e8284e877d2846685acb8bb165f1cfc96deea4688a4cf26c23"
|
||||
},
|
||||
"python-qualified-constructor/main.py": {
|
||||
"captureGroups": 9,
|
||||
|
|
@ -712,8 +716,8 @@
|
|||
"digest": "b397708c05d101d0d3978313f65184c9f181a179b803d242b1e585ac2605fe4a"
|
||||
},
|
||||
"python-static-class-methods/service.py": {
|
||||
"captureGroups": 27,
|
||||
"digest": "9539dd5884b6b5ef3ad14b9d06837a495b4d3908462ec37464524c259a2cbd75"
|
||||
"captureGroups": 32,
|
||||
"digest": "91730faa59b93ee5a06c8f7050831d32f39ad89c624cb665a4eeb38449aa2c3e"
|
||||
},
|
||||
"python-super-resolution/models/__init__.py": {
|
||||
"captureGroups": 0,
|
||||
|
|
@ -728,8 +732,8 @@
|
|||
"digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215"
|
||||
},
|
||||
"python-super-resolution/models/user.py": {
|
||||
"captureGroups": 11,
|
||||
"digest": "8a66f8962fcf960b66c1106d1d67da21f7f6bac323b961e5fc0623b3c93dbc38"
|
||||
"captureGroups": 12,
|
||||
"digest": "8faf7b3a238f654f3ae2231cd5a7d968b2604481581bc2e9cb07897e5027011f"
|
||||
},
|
||||
"python-variadic-resolution/app.py": {
|
||||
"captureGroups": 5,
|
||||
|
|
@ -768,7 +772,7 @@
|
|||
"digest": "0aa940e3428c35b1324544e9bcc75c166732b63b3e65e6faf460bc3d3a9571ec"
|
||||
},
|
||||
"synthetic:dao-20": {
|
||||
"captureGroups": 473,
|
||||
"digest": "27a7e0ea629f7ed002f1bd254126c819125900b27e0818d879e8fdaa8690aab9"
|
||||
"captureGroups": 493,
|
||||
"digest": "3ef7932339801c2cfa24609b741b472bed2f1037b413edafaadf8b8c2edd8f7c"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* Regression tests for Python scope-resolution coverage gaps (issue #1932).
|
||||
*
|
||||
* Each fixture FAILS on main and PASSES on the fix branch.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { emitPythonScopeCaptures } from '../../../src/core/ingestion/languages/python/index.js';
|
||||
import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js';
|
||||
import { pythonProvider } from '../../../src/core/ingestion/languages/python.js';
|
||||
import type { CaptureMatch } from 'gitnexus-shared';
|
||||
|
||||
/**
|
||||
* Count matches whose capture-key set satisfies `predicate`.
|
||||
*/
|
||||
function countCaptures(src: string, predicate: (tags: string[]) => boolean): number {
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
return matches.filter((m) => predicate(Object.keys(m))).length;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F57 — Heritage: qualified/subscripted bases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F57 — Python heritage (qualified / subscripted bases)', () => {
|
||||
it('bare identifier base class emits @heritage.class + @heritage.extends', () => {
|
||||
const src = `
|
||||
class Base:
|
||||
pass
|
||||
|
||||
class Child(Base):
|
||||
pass
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const heritageMatches = matches.filter((m) => m['@heritage.class']);
|
||||
expect(heritageMatches.length).toBe(1);
|
||||
expect(heritageMatches[0]['@heritage.class'].text).toBe('Child');
|
||||
expect(heritageMatches[0]['@heritage.extends'].text).toBe('Base');
|
||||
});
|
||||
|
||||
it('qualified base (mod.Class) emits @heritage.extends for attribute', () => {
|
||||
const src = `
|
||||
class A(mod.Base):
|
||||
pass
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const heritageMatches = matches.filter((m) => m['@heritage.class']);
|
||||
expect(heritageMatches.length).toBe(1);
|
||||
expect(heritageMatches[0]['@heritage.class'].text).toBe('A');
|
||||
expect(heritageMatches[0]['@heritage.extends'].text).toBe('mod.Base');
|
||||
});
|
||||
|
||||
it('subscripted base (Generic[T]) emits @heritage.extends for subscript', () => {
|
||||
const src = `
|
||||
from typing import Generic, TypeVar
|
||||
T = TypeVar('T')
|
||||
|
||||
class B(Generic[T]):
|
||||
pass
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const heritageMatches = matches.filter((m) => m['@heritage.class']);
|
||||
expect(heritageMatches.length).toBe(1);
|
||||
expect(heritageMatches[0]['@heritage.class'].text).toBe('B');
|
||||
expect(heritageMatches[0]['@heritage.extends'].text).toBe('Generic[T]');
|
||||
});
|
||||
|
||||
it('multiple patterns coexist with bare-identifier heritage', () => {
|
||||
const src = `
|
||||
class C(types.Type):
|
||||
pass
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const heritageMatches = matches.filter((m) => m['@heritage.class']);
|
||||
expect(heritageMatches.length).toBe(1);
|
||||
expect(heritageMatches[0]['@heritage.class'].text).toBe('C');
|
||||
expect(heritageMatches[0]['@heritage.extends'].text).toBe('types.Type');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F58 — Decorator captures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F58 — Python decorator captures', () => {
|
||||
it('simple @app.route decorator emits @reference.call.member', () => {
|
||||
const src = `
|
||||
@app.route("/")
|
||||
def index():
|
||||
return "ok"
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const decoratorMatches = matches.filter((m) => m['@reference.call.member']);
|
||||
expect(decoratorMatches.length).toBe(1);
|
||||
expect(decoratorMatches[0]['@reference.name']?.text).toBe('route');
|
||||
});
|
||||
|
||||
it('nested attribute decorator @api.v1.endpoint emits @reference.call.member', () => {
|
||||
const src = `
|
||||
@api.v1.endpoint
|
||||
def handler():
|
||||
pass
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const decoratorMatches = matches.filter((m) => m['@reference.call.member']);
|
||||
expect(decoratorMatches.length).toBe(1);
|
||||
expect(decoratorMatches[0]['@reference.name']?.text).toBe('endpoint');
|
||||
});
|
||||
|
||||
it('simple @decorator (bare identifier) emits @reference.call.free', () => {
|
||||
const src = `
|
||||
@login_required
|
||||
def protected_view():
|
||||
pass
|
||||
`;
|
||||
const matches = emitPythonScopeCaptures(src, 'test.py') as CaptureMatch[];
|
||||
const decoratorMatches = matches.filter((m) => m['@reference.call.free']);
|
||||
expect(decoratorMatches.length).toBe(1);
|
||||
expect(decoratorMatches[0]['@reference.name']?.text).toBe('login_required');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F58 — End-to-end: extractParsedFile produces referenceSites
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F58 — decorator produces referenceSites in extractParsedFile', () => {
|
||||
it('@login_required produces a referenceSite entry', () => {
|
||||
const src = `@login_required\ndef foo():\n pass\n`;
|
||||
const parsedFile = extractParsedFile(pythonProvider, src, 'app.py', () => {});
|
||||
expect(parsedFile).not.toBeNull();
|
||||
expect(parsedFile!.referenceSites.length).toBeGreaterThanOrEqual(1);
|
||||
const hasLoginRef = parsedFile!.referenceSites.some((r) => r.name === 'login_required');
|
||||
expect(hasLoginRef).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F61 — Lambda scope
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('F61 — Python lambda scope', () => {
|
||||
it('bare lambda emits @scope.function', () => {
|
||||
const src = `handler = lambda x: x + 1\n`;
|
||||
const scopeFnCount = countCaptures(src, (tags) => tags.includes('@scope.function'));
|
||||
expect(scopeFnCount).toBe(1);
|
||||
});
|
||||
|
||||
it('multiple lambdas each get their own @scope.function', () => {
|
||||
const src = `double = lambda x: x * 2\ntriple = lambda x: x * 3\n`;
|
||||
const scopeFnCount = countCaptures(src, (tags) => tags.includes('@scope.function'));
|
||||
expect(scopeFnCount).toBe(2);
|
||||
});
|
||||
|
||||
it('lambda coexists with function_definition scopes', () => {
|
||||
const src = `
|
||||
def normal(x):
|
||||
return x + 1
|
||||
|
||||
handler = lambda x: x * 2
|
||||
`;
|
||||
const scopeFnCount = countCaptures(src, (tags) => tags.includes('@scope.function'));
|
||||
expect(scopeFnCount).toBe(2);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue