mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
Merge branch 'main' into codex/prune-local-const-symbols
This commit is contained in:
commit
d10bbf36b6
2 changed files with 169 additions and 1 deletions
|
|
@ -55,6 +55,7 @@ const METHOD_ANNOTATION_TO_HTTP: Record<string, string> = {
|
|||
interface SpringRouteBinding {
|
||||
method: string;
|
||||
path: string;
|
||||
ownerPrefix?: string;
|
||||
}
|
||||
|
||||
interface SpringMethodInfo {
|
||||
|
|
@ -395,6 +396,25 @@ function joinPath(prefix: string, methodPath: string): string {
|
|||
return `/${cleanPrefix}/${cleanSub}`;
|
||||
}
|
||||
|
||||
function joinInheritedSpringPath(
|
||||
controllerPrefix: string,
|
||||
inheritedPath: string,
|
||||
inheritedOwnerPrefix = '',
|
||||
): string {
|
||||
const joined = joinPath(controllerPrefix, inheritedPath);
|
||||
const cleanPrefix = controllerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const cleanOwnerPrefix = inheritedOwnerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const cleanInherited = inheritedPath.replace(/^\/+/, '');
|
||||
if (!cleanPrefix) return joined;
|
||||
if (
|
||||
cleanPrefix === cleanOwnerPrefix &&
|
||||
(cleanInherited === cleanPrefix || cleanInherited.startsWith(`${cleanPrefix}/`))
|
||||
) {
|
||||
return `/${cleanInherited}`;
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
|
||||
function getNodeName(node: Parser.SyntaxNode): string | null {
|
||||
return node.childForFieldName('name')?.text ?? null;
|
||||
}
|
||||
|
|
@ -634,6 +654,7 @@ function scanSpringProject(files: readonly HttpScanInput[]): HttpFileDetections[
|
|||
const routes = method.routes.map((route) => ({
|
||||
method: route.method,
|
||||
path: type.classPrefix ? joinPath(type.classPrefix, route.path) : route.path,
|
||||
ownerPrefix: type.classPrefix,
|
||||
}));
|
||||
if (routes.length > 0) methodMap.set(method.name, routes);
|
||||
}
|
||||
|
|
@ -651,7 +672,7 @@ function scanSpringProject(files: readonly HttpScanInput[]): HttpFileDetections[
|
|||
const routes = routeMap.get(method.name) ?? [];
|
||||
return routes.map((route) => ({
|
||||
method: route.method,
|
||||
path: joinPath(type.classPrefix, route.path),
|
||||
path: joinInheritedSpringPath(type.classPrefix, route.path, route.ownerPrefix),
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -952,6 +952,153 @@ public class UserController implements UserApi {
|
|||
);
|
||||
});
|
||||
|
||||
it('does not duplicate inherited Spring prefixes already present on the controller', async () => {
|
||||
const dir = path.join(tmpDir, 'spring-inherited-prefix-dedup');
|
||||
fs.mkdirSync(path.join(dir, 'src/rest'), { recursive: true });
|
||||
fs.mkdirSync(path.join(dir, 'src/controller'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/rest/DataReleaseFacade.java'),
|
||||
`
|
||||
package com.example.rest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RequestMapping("/open/ai")
|
||||
public interface DataReleaseFacade {
|
||||
@GetMapping("/query")
|
||||
Object query();
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/controller/BaseFacadeService.java'),
|
||||
`
|
||||
package com.example.controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RequestMapping("/open/ai")
|
||||
public abstract class BaseFacadeService {
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/controller/DataReleaseFacadeImpl.java'),
|
||||
`
|
||||
package com.example.controller;
|
||||
import com.example.rest.DataReleaseFacade;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/open/ai")
|
||||
public class DataReleaseFacadeImpl extends BaseFacadeService implements DataReleaseFacade {
|
||||
@Override
|
||||
public Object query() { return null; }
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const providers = contracts.filter((c) => c.role === 'provider');
|
||||
|
||||
const queryRoute = providers.find((c) => c.contractId === 'http::GET::/open/ai/query');
|
||||
expect(queryRoute).toBeDefined();
|
||||
expect(toPosixPath(queryRoute!.symbolRef.filePath)).toBe(
|
||||
'src/controller/DataReleaseFacadeImpl.java',
|
||||
);
|
||||
expect(providers.find((c) => c.contractId === 'http::GET::/open/ai/open/ai/query')).toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('still combines distinct inherited Spring prefixes that share a leading segment', async () => {
|
||||
const dir = path.join(tmpDir, 'spring-interface-shared-leading-prefix');
|
||||
fs.mkdirSync(path.join(dir, 'src/rest'), { recursive: true });
|
||||
fs.mkdirSync(path.join(dir, 'src/controller'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/rest/DataReleaseApi.java'),
|
||||
`
|
||||
package com.example.rest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RequestMapping("/open/ai")
|
||||
public interface DataReleaseApi {
|
||||
@GetMapping("/query")
|
||||
Object query();
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/controller/DataReleaseFacadeImpl.java'),
|
||||
`
|
||||
package com.example.controller;
|
||||
import com.example.rest.DataReleaseApi;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/open")
|
||||
public class DataReleaseFacadeImpl implements DataReleaseApi {
|
||||
@Override
|
||||
public Object query() { return null; }
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const providers = contracts.filter((c) => c.role === 'provider');
|
||||
|
||||
expect(
|
||||
providers.find((c) => c.contractId === 'http::GET::/open/open/ai/query'),
|
||||
).toBeDefined();
|
||||
expect(providers.find((c) => c.contractId === 'http::GET::/open/ai/query')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps a controller prefix when a prefix-less interface method starts with the same path', async () => {
|
||||
const dir = path.join(tmpDir, 'spring-interface-method-prefix-overlap');
|
||||
fs.mkdirSync(path.join(dir, 'src/rest'), { recursive: true });
|
||||
fs.mkdirSync(path.join(dir, 'src/controller'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/rest/UserApi.java'),
|
||||
`
|
||||
package com.example.rest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
public interface UserApi {
|
||||
@GetMapping("/users/{id}")
|
||||
Object getUser();
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'src/controller/UserController.java'),
|
||||
`
|
||||
package com.example.controller;
|
||||
import com.example.rest.UserApi;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController implements UserApi {
|
||||
@Override
|
||||
public Object getUser() { return null; }
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const contracts = await extractor.extract(null, dir, makeRepo(dir));
|
||||
const providers = contracts.filter((c) => c.role === 'provider');
|
||||
|
||||
expect(
|
||||
providers.find((c) => c.contractId === 'http::GET::/users/users/{param}'),
|
||||
).toBeDefined();
|
||||
expect(providers.find((c) => c.contractId === 'http::GET::/users/{param}')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips ambiguous inherited routes when interfaces share a simple name', async () => {
|
||||
const dir = path.join(tmpDir, 'spring-interface-simple-name-collision');
|
||||
fs.mkdirSync(path.join(dir, 'src/a'), { recursive: true });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue