python-module-scope-collection-methods.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  2. import * as fs from 'fs';
  3. import * as os from 'os';
  4. import * as path from 'path';
  5. import { CodeGraph } from '../src';
  6. const collections = [
  7. { name: 'dict_literal', value: '{"answer": "42"}', method: 'get' },
  8. { name: 'empty_dict', value: '{}', method: 'get' },
  9. { name: 'dict_constructor', value: 'dict()', method: 'get' },
  10. { name: 'list_literal', value: '[1]', method: 'append' },
  11. { name: 'empty_list', value: '[]', method: 'append' },
  12. { name: 'list_constructor', value: 'list()', method: 'append' },
  13. { name: 'set_literal', value: '{1}', method: 'add' },
  14. { name: 'set_constructor', value: 'set()', method: 'add' },
  15. { name: 'tuple_literal', value: '(1,)', method: 'index' },
  16. { name: 'empty_tuple', value: '()', method: 'index' },
  17. { name: 'tuple_constructor', value: 'tuple()', method: 'index' },
  18. { name: 'frozenset_constructor', value: 'frozenset()', method: 'union' },
  19. ];
  20. let dir: string;
  21. let cg: CodeGraph;
  22. beforeAll(async () => {
  23. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1652-'));
  24. fs.writeFileSync(path.join(dir, 'settings.py'), `DEFAULTS = {"answer": "42"}
  25. def read_setting(name):
  26. return DEFAULTS.get(name, None)
  27. `);
  28. fs.writeFileSync(path.join(dir, 'cache.py'), `class LRUCache:
  29. def __init__(self):
  30. self._store = {}
  31. def get(self, key):
  32. return self._store.get(key)
  33. class ProjectCollection:
  34. def append(self, item):
  35. pass
  36. def add(self, item):
  37. pass
  38. def index(self, item):
  39. return 0
  40. def union(self, item):
  41. return self
  42. `);
  43. for (const { name, value, method } of collections) {
  44. // Capitalizing lRUCache matches a real class. Its name must not override
  45. // the same-file binding's collection initializer (#1652).
  46. fs.writeFileSync(path.join(dir, `${name}.py`), `lRUCache = ${value}
  47. def use_${name}(item):
  48. return lRUCache.${method}(item)
  49. `);
  50. }
  51. fs.writeFileSync(path.join(dir, 'unknown.py'), `UNKNOWN = load_defaults()
  52. def read_unknown(name):
  53. return UNKNOWN.get(name)
  54. `);
  55. fs.writeFileSync(path.join(dir, 'client.py'), `from cache import LRUCache
  56. def read_cache(lRUCache: LRUCache, name):
  57. return lRUCache.get(name)
  58. `);
  59. cg = await CodeGraph.init(dir, { index: true });
  60. });
  61. afterAll(() => {
  62. cg?.destroy();
  63. fs.rmSync(dir, { recursive: true, force: true });
  64. });
  65. function expectNoMethodCall(callerName: string, file: string, methodName: string) {
  66. const caller = cg.getNodesByName(callerName).find((n) => n.kind === 'function' && n.filePath === file);
  67. const method = cg.getNodesByName(methodName).find((n) => n.kind === 'method' && n.filePath === 'cache.py');
  68. expect(caller).toBeDefined();
  69. expect(method).toBeDefined();
  70. expect(cg.getCallers(method!.id).map(({ node }) => node.id)).not.toContain(caller!.id);
  71. expect(cg.getCallees(caller!.id).map(({ node }) => node.id)).not.toContain(method!.id);
  72. }
  73. describe('Python module-scope collection methods (#1652)', () => {
  74. it('does not connect DEFAULTS.get to the unrelated LRUCache.get method', () => {
  75. expectNoMethodCall('read_setting', 'settings.py', 'get');
  76. });
  77. it.each(collections)('keeps $name ($method) external even when the receiver resembles a class', ({ name, method }) => {
  78. expectNoMethodCall(`use_${name}`, `${name}.py`, method);
  79. });
  80. it('does not treat an unrelated variable as evidence of a project class', () => {
  81. expectNoMethodCall('read_unknown', 'unknown.py', 'get');
  82. });
  83. it('preserves real instance calls despite same-named collections in other files', () => {
  84. const caller = cg.getNodesByName('read_cache').find((n) => n.kind === 'function')!;
  85. const method = cg.getNodesByName('get').find((n) => n.kind === 'method' && n.filePath === 'cache.py')!;
  86. expect(caller).toBeDefined();
  87. expect(method).toBeDefined();
  88. expect(cg.getCallees(caller.id).map(({ node }) => node.id)).toContain(method.id);
  89. expect(cg.getCallers(method.id).map(({ node }) => node.id)).toContain(caller.id);
  90. });
  91. });