mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
* [+] Add django route discovery to create cross-link for multi-repo * [+] Update ingestion * [~] Fix bugs and abstraction violation * feat(python-http): add keyword url= and variable propagation for consumer detection - Add REQUESTS_KEYWORD_URL_PATTERNS for requests.get(url='...') keyword args - Add WRAPPER_URI_PATTERNS for generic wrapper.fetch(uri='...') calls - Add WRAPPER_URI_VAR_PATTERNS + buildLocalStringMap for uri=variable propagation - Add LOCAL_STRING_ASSIGNMENTS to track uri='...' assignments - Wire both direct-string and variable-propagation loops in scan() - Add normalizeConsumerPath() helper Note: Automatic cross-link detection remains limited for runtime-computed URLs (URLs built via .format(), string concat, or module constants). Manual manifest links needed for known cross-repo contracts. * [+] add extract uri and url keywork pattern for request http * feat(python-http): add variable propagation for uri=/url= consumer patterns Re-add LOCAL_STRING_ASSIGNMENTS, WRAPPER_URI_VAR_PATTERNS, buildLocalStringMap(), and normalizeConsumerPath() lost during cherry-pick merge of upstream keyword-URL commit. Together with the upstream WRAPPER_URI_PATTERNS and REQUESTS_KEYWORD_URL_PATTERNS, we now detect: - requests.get(url='literal') keyword args - wrapper.fetch(uri='literal') keyword args - wrapper.fetch(uri=variable) where variable was assigned a string literal * fix(group): discover Django roots relative to manage.py dir + multi-project (#1836 R1) A Django project not at the repo root (e.g. backend/manage.py) discovered zero routes: the settings module path was resolved repo-root-relative only, so backend/myproj/settings.py was never found and discovery returned null. Resolve settings, star-imported base settings, ROOT_URLCONF, and the root urls.py against the manage.py's own directory first, then the repo root (resolvedSettingsPath is now project-dir-aware so relative imports anchor correctly). Iterate every manage.py so a monorepo with several Django projects yields each project's root — the provider hook becomes plural (discoverRootRouteFiles → string[]) and the main-thread pass loops over all roots (inner-scoped continues, parser hoisted once per language). Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(group): remove dead code in Django root discovery (#1836 R9) - Collapse the identical if/else in extractStarImports to one push. - Drop the unreachable baseModule.startsWith('.') branch (baseModule is always a resolved slash-path or a bare absolute module — never dot-prefixed). - Import DjangoFileReader from django.ts instead of re-declaring the type. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(group): walk Django includes once per prefix, not per file (#1836 R2) The include() recursion guard was keyed on file path alone and shared across the whole walk, so a urlconf included under two prefixes (a "diamond" — the same app mounted at /v1/ and /v2/) emitted routes for only the first mount. Key the guard on (resolvedFilePath, accumulatedPrefix) at all three sites (function entry, path()-wrapped include, bare include) so a file reached under two distinct prefixes is walked once per prefix while a genuine cycle (same file + same prefix) still terminates — null/'' prefixes collapse to one key so a no-prefix re-entry is treated as a cycle. MAX_INCLUDE_DEPTH remains the backstop. Adds diamond + self-include-cycle tests. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(group): extract Django routes from non-list urlpatterns (#1836 R3) findUrlpatternsLists only accepted a list-literal RHS, so common shapes yielded zero routes: concatenation (urlpatterns = a + b), wrapper calls (format_suffix_patterns([...]), i18n_patterns, staticfiles_urlpatterns), and tuples. Add collectUrlpatternContainers to descend binary_operator operands, known wrapper-call list arguments, and tuples. Inherently-dynamic forms (DRF router.urls, comprehensions, bare names) still yield nothing but now emit a debug log so the silent-zero case is observable rather than mysterious. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(group): thread Django parser explicitly, drop module singleton (#1836 R4) extractDjangoRoutes relied on a module-level _djangoParser set via setDjangoParser before each call — hidden state that would break if a second language ever used the include re-parse path, and an easy-to-forget contract. Pass the tree-sitter parser as an explicit parameter of extractDjangoRoutes (the extractRoutes provider hook already receives it) and delete the global plus its setter. The Python provider wires it directly; tests pass the parser in place of the removed setDjangoParser() call. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ingestion): isolate a throwing extractRoutes in the cross-file route pass (#1836 R5) The main-thread cross-file route pass called provider.extractRoutes without a guard, so a throw (e.g. a future grammar edge case in the include() walk) would propagate out of the parse phase and abort the entire analyze — unlike the worker, which isolates per-file failures. Wrap the per-root extractRoutes call in try/catch that logs a warning and continues to the next root. Export extractCrossFileRoutes and add a unit test driving a stub provider whose extractRoutes throws, asserting the pass returns [] and does not propagate. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(ingestion): bucket only route-capable languages in cross-file pass (#1836 R6) extractCrossFileRoutes runs in the deferred band on every analyze (incl. warm all-cache-hit runs). It now derives the set of languages whose provider exposes the cross-file route hooks once, returns early if none do, and buckets only those languages' paths — so a non-framework repo no longer pays to bucket the languages it doesn't use here. Route results are intentionally not persisted across runs, so a Django repo still re-derives its routes each analyze; documented inline that cross-run route caching is a deliberate follow-up rather than implemented here. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(group): prettier-format http-patterns/python.ts (#1836 R7) The file was not formatted to the root .prettierrc (the consumer-path normalizer used single-line try/catch and method chains), so the CI quality/format check (`prettier --check .`) failed. Reflow only — no logic change (`git diff -w` confines the change to normalizeConsumerPath's layout). Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(group): dedup Python URI detections by byte offset, not line arithmetic (#1836 R8) The wrapper-URI dedup key was lineNum*1000+methodRow, which can collide for distinct calls in files over 1000 lines (carry into the row term) and can fail to dedup a genuine duplicate when a node straddles a line boundary. Key on node byte offsets (`${pathNode.startIndex}:${methodNode.startIndex}`), matching the sibling seenVarDetections dedup a few lines below. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(ingestion): end-to-end Django cross-file route extraction (#1836 R10) Adds an integration test that runs runPipelineFromRepo against a Django fixture whose project lives under backend/, asserting the resulting Route graph nodes (/health, /api/items, /api/items/<int:pk>). This exercises the previously-untested main-thread orchestration glue (discovery → parse → extractRoutes → allExtractedRoutes → Route nodes) and, because the project is in a subdirectory, regresses the subdir-discovery fix (R1) — a repo-root-only resolver would discover nothing and emit zero Route nodes. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(group): anchor Django include() resolution at the project root (#1836 review F1) resolveIncludedFile tried the bare repo-root candidate (app/urls.py) before the project-relative one, so in a monorepo with both a repo-root app/ and a backend/ Django project that also has an app/, include('app.urls') from the backend project resolved to the WRONG service's routes. Probe up-tree from the root urls.py for the nearest manage.py (the Django project root / sys.path entry) and try that-anchored candidate first. Absolute module paths like `app.urls` now resolve to <projectRoot>/app/urls.py unambiguously. When no manage.py is reachable (e.g. unit tests with a urls-only reader) the prior strategy order is preserved. Adds a monorepo wrong-app test. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(group): drop bogus Django provider source-scan, use graph routes (#1836 review F2) The DJANGO_PATH_PATTERNS / DJANGO_URL_PATTERNS source scan emitted an HTTP provider contract for every path()/re_path()/url() string literal, without checking it was inside urlpatterns, without skipping include() mount points, and without composing the include() prefix across files. For `path('api/', include('app.urls'))` + child `path('items/', view)` it emitted providers for `/api` (a mount, not a route) and `/items` (un-prefixed) — which survived the exact-contract-ID dedup alongside the correct graph route `/api/items`, polluting cross-repo matching with false providers. Remove the Django provider patterns and their scan blocks. Django provider contracts come from the graph Route nodes, which the ingestion route extractor builds with includes already composed (and now correctly, per the other fixes). Python HTTP *consumer* patterns (requests/wrapper) are unaffected. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(group): match method-agnostic Django providers to any-method consumers (#1836 review F3) Django function views are method-agnostic, so extractDjangoRoutes emits httpMethod '*'. That '*' was dropped by normalizeRouteMethod and then defaulted to GET by the contract extractor, while the matcher only expanded wildcard *consumers* — so a `POST /api/items` consumer never matched the Django provider that was silently narrowed to GET. - routes.ts: preserve '*' as a method-agnostic marker on the Route node, so the contract layer emits a wildcard provider (http::*::path) instead of GET. - matching.ts: make findMatchingKeys symmetric — a specific-method consumer now matches an exact-method provider OR a wildcard (http::*::) provider on the same path, mirroring the existing wildcard-consumer expansion. Co-Authored-By: HuyNguyenDinh <61400397+HuyNguyenDinh@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Dinh Huy <huynd86@fpt.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
5.1 KiB
TypeScript
117 lines
5.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { discoverDjangoRootUrls } from '../../src/core/ingestion/route-extractors/django-root-discovery.js';
|
|
|
|
/** Build a disk-style reader from a path → content record. */
|
|
const makeReader = (fsMap: Record<string, string>) => (relativePath: string) =>
|
|
Object.prototype.hasOwnProperty.call(fsMap, relativePath) ? fsMap[relativePath] : null;
|
|
|
|
/** A `manage.py` body pointing at the given dotted settings module. */
|
|
const manageFor = (settingsModule: string) =>
|
|
`#!/usr/bin/env python\nimport os\ndef main():\n os.environ.setdefault('DJANGO_SETTINGS_MODULE', '${settingsModule}')\n`;
|
|
|
|
const MANAGE_PY = manageFor('myproj.settings');
|
|
|
|
describe('discoverDjangoRootUrls', () => {
|
|
it('discovers the root urls.py from content-bearing files (no reader)', () => {
|
|
const files = [
|
|
{ path: 'manage.py', content: MANAGE_PY },
|
|
{ path: 'myproj/settings.py', content: `ROOT_URLCONF = 'myproj.urls'\n` },
|
|
{ path: 'myproj/urls.py', content: `urlpatterns = []\n` },
|
|
];
|
|
expect(discoverDjangoRootUrls(files)).toEqual(['myproj/urls.py']);
|
|
});
|
|
|
|
it('discovers the root urls.py via the reader fallback when files carry no content', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'manage.py': MANAGE_PY,
|
|
'myproj/settings.py': `ROOT_URLCONF = 'myproj.urls'\n`,
|
|
'myproj/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
// Only paths are passed (the main-thread pass does this); content is resolved on demand.
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual(['myproj/urls.py']);
|
|
});
|
|
|
|
it('follows ROOT_URLCONF through a star-imported base settings module via the reader', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'manage.py': MANAGE_PY,
|
|
'myproj/settings.py': `from .base import *\n`,
|
|
'myproj/base.py': `DEBUG = True\nROOT_URLCONF = 'myproj.urls'\n`,
|
|
'myproj/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual(['myproj/urls.py']);
|
|
});
|
|
|
|
it('resolves a urls package directory module (urls/__init__.py)', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'manage.py': MANAGE_PY,
|
|
'myproj/settings.py': `ROOT_URLCONF = 'myproj.urls'\n`,
|
|
'myproj/urls/__init__.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual([
|
|
'myproj/urls/__init__.py',
|
|
]);
|
|
});
|
|
|
|
it('discovers a Django project located in a subdirectory (settings resolved relative to manage.py)', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'backend/manage.py': MANAGE_PY,
|
|
'backend/myproj/settings.py': `ROOT_URLCONF = 'myproj.urls'\n`,
|
|
'backend/myproj/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual([
|
|
'backend/myproj/urls.py',
|
|
]);
|
|
});
|
|
|
|
it('follows star-imported base settings for a subdirectory project', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'backend/manage.py': MANAGE_PY,
|
|
'backend/myproj/settings.py': `from .base import *\n`,
|
|
'backend/myproj/base.py': `ROOT_URLCONF = 'myproj.urls'\n`,
|
|
'backend/myproj/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual([
|
|
'backend/myproj/urls.py',
|
|
]);
|
|
});
|
|
|
|
it('discovers every project in a monorepo with multiple manage.py files', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'serviceA/manage.py': manageFor('svca.settings'),
|
|
'serviceA/svca/settings.py': `ROOT_URLCONF = 'svca.urls'\n`,
|
|
'serviceA/svca/urls.py': `urlpatterns = []\n`,
|
|
'serviceB/manage.py': manageFor('svcb.settings'),
|
|
'serviceB/svcb/settings.py': `ROOT_URLCONF = 'svcb.urls'\n`,
|
|
'serviceB/svcb/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual([
|
|
'serviceA/svca/urls.py',
|
|
'serviceB/svcb/urls.py',
|
|
]);
|
|
});
|
|
|
|
it('returns an empty array when there is no manage.py', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'myproj/settings.py': `ROOT_URLCONF = 'myproj.urls'\n`,
|
|
'myproj/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual([]);
|
|
});
|
|
|
|
it('returns an empty array when ROOT_URLCONF cannot be found in settings', () => {
|
|
const fsMap: Record<string, string> = {
|
|
'manage.py': MANAGE_PY,
|
|
'myproj/settings.py': `DEBUG = True\n`,
|
|
'myproj/urls.py': `urlpatterns = []\n`,
|
|
};
|
|
const files = Object.keys(fsMap).map((path) => ({ path }));
|
|
expect(discoverDjangoRootUrls(files, undefined, makeReader(fsMap))).toEqual([]);
|
|
});
|
|
});
|