nogic 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nogic/parsing/types.py ADDED
@@ -0,0 +1,80 @@
1
+ """
2
+ Data classes for extracted code elements.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from tree_sitter import Node
10
+
11
+
12
+ @dataclass
13
+ class ExtractedFunction:
14
+ """Extracted function or method information."""
15
+
16
+ name: str
17
+ qualified_name: str
18
+ start_line: int
19
+ end_line: int
20
+ signature: str
21
+ docstring: str | None = None
22
+ decorators: list[str] = field(default_factory=list)
23
+ is_async: bool = False
24
+ is_method: bool = False
25
+ class_name: str | None = None
26
+ parameters: list[str] = field(default_factory=list)
27
+ return_type: str | None = None
28
+ source_code: str = ""
29
+ node: "Node | None" = field(default=None, repr=False)
30
+
31
+
32
+ @dataclass
33
+ class ExtractedClass:
34
+ """Extracted class information."""
35
+
36
+ name: str
37
+ qualified_name: str
38
+ start_line: int
39
+ end_line: int
40
+ docstring: str | None = None
41
+ decorators: list[str] = field(default_factory=list)
42
+ bases: list[str] = field(default_factory=list)
43
+ methods: list[ExtractedFunction] = field(default_factory=list)
44
+ source_code: str = ""
45
+ node: "Node | None" = field(default=None, repr=False)
46
+
47
+
48
+ @dataclass
49
+ class ExtractedCall:
50
+ """Extracted function call information."""
51
+
52
+ name: str
53
+ line: int
54
+ caller_qualified_name: str
55
+ receiver: str | None = None
56
+ arguments: list[str] = field(default_factory=list)
57
+
58
+
59
+ @dataclass
60
+ class ExtractedImport:
61
+ """Extracted import statement."""
62
+
63
+ module: str
64
+ name: str | None = None
65
+ alias: str | None = None
66
+ is_wildcard: bool = False
67
+ line: int = 0
68
+
69
+
70
+ @dataclass
71
+ class ParseResult:
72
+ """Container for all extracted elements from a file."""
73
+
74
+ file_path: str
75
+ language: str
76
+ functions: list[ExtractedFunction] = field(default_factory=list)
77
+ classes: list[ExtractedClass] = field(default_factory=list)
78
+ imports: list[ExtractedImport] = field(default_factory=list)
79
+ calls: list[ExtractedCall] = field(default_factory=list)
80
+ error: str | None = None
@@ -0,0 +1,14 @@
1
+ """
2
+ Storage module for SQLite database operations.
3
+ """
4
+
5
+ from .schema import create_schema, SCHEMA_VERSION
6
+ from .symbols import SymbolStorageService
7
+ from .relationships import RelationshipStorageService
8
+
9
+ __all__ = [
10
+ "create_schema",
11
+ "SCHEMA_VERSION",
12
+ "SymbolStorageService",
13
+ "RelationshipStorageService",
14
+ ]
@@ -0,0 +1,322 @@
1
+ """
2
+ Relationship storage service for managing code relationships in SQLite.
3
+ """
4
+
5
+ import json
6
+ import sqlite3
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+ from nogic.parsing.types import ExtractedCall, ExtractedImport, ExtractedClass
11
+
12
+
13
+ @dataclass
14
+ class StoredImport:
15
+ """A file-level import retrieved from the database."""
16
+
17
+ id: int
18
+ source_file_id: int
19
+ target_file_id: Optional[int]
20
+ module_name: str
21
+
22
+
23
+ @dataclass
24
+ class StoredSymbolImport:
25
+ """A symbol-level import retrieved from the database."""
26
+
27
+ id: int
28
+ source_file_id: int
29
+ module_name: str
30
+ symbol_names: list[str]
31
+ import_type: str
32
+ alias: Optional[str]
33
+ line: Optional[int]
34
+
35
+
36
+ @dataclass
37
+ class StoredCall:
38
+ """A call relationship retrieved from the database."""
39
+
40
+ id: int
41
+ caller_id: str
42
+ callee_id: Optional[str]
43
+ callee_name: str
44
+ call_type: str
45
+ receiver: Optional[str]
46
+ line: Optional[int]
47
+
48
+
49
+ @dataclass
50
+ class StoredInheritance:
51
+ """An inheritance relationship retrieved from the database."""
52
+
53
+ id: int
54
+ child_id: str
55
+ parent_name: str
56
+ parent_id: Optional[str]
57
+ type: str
58
+
59
+
60
+ class RelationshipStorageService:
61
+ """Service for storing and querying code relationships."""
62
+
63
+ def __init__(self, conn: sqlite3.Connection):
64
+ self.conn = conn
65
+ self.conn.row_factory = sqlite3.Row
66
+
67
+ # --- Import Storage ---
68
+
69
+ def store_import(
70
+ self,
71
+ source_file_id: int,
72
+ imp: ExtractedImport,
73
+ target_file_id: Optional[int] = None,
74
+ ) -> None:
75
+ """Store an import relationship."""
76
+ # Store file-level import
77
+ self.conn.execute(
78
+ """
79
+ INSERT OR IGNORE INTO imports (source_file_id, target_file_id, module_name)
80
+ VALUES (?, ?, ?)
81
+ """,
82
+ (source_file_id, target_file_id, imp.module),
83
+ )
84
+
85
+ # Determine import type
86
+ if imp.is_wildcard:
87
+ import_type = "wildcard"
88
+ symbol_names = ["*"]
89
+ elif imp.name:
90
+ import_type = "named"
91
+ symbol_names = [imp.name]
92
+ else:
93
+ import_type = "namespace"
94
+ symbol_names = [imp.module.split(".")[-1]]
95
+
96
+ # Store symbol-level import
97
+ self.conn.execute(
98
+ """
99
+ INSERT INTO symbol_imports (source_file_id, module_name, symbol_names, import_type, alias, line)
100
+ VALUES (?, ?, ?, ?, ?, ?)
101
+ """,
102
+ (
103
+ source_file_id,
104
+ imp.module,
105
+ json.dumps(symbol_names),
106
+ import_type,
107
+ imp.alias,
108
+ imp.line,
109
+ ),
110
+ )
111
+
112
+ def delete_file_imports(self, file_id: int) -> int:
113
+ """Delete all imports for a file. Returns count deleted."""
114
+ count1 = self.conn.execute(
115
+ "DELETE FROM imports WHERE source_file_id = ?", (file_id,)
116
+ ).rowcount
117
+ count2 = self.conn.execute(
118
+ "DELETE FROM symbol_imports WHERE source_file_id = ?", (file_id,)
119
+ ).rowcount
120
+ return count1 + count2
121
+
122
+ def get_file_imports(self, file_id: int) -> list[StoredImport]:
123
+ """Get all file-level imports for a file."""
124
+ rows = self.conn.execute(
125
+ "SELECT * FROM imports WHERE source_file_id = ?", (file_id,)
126
+ ).fetchall()
127
+
128
+ return [
129
+ StoredImport(
130
+ id=row["id"],
131
+ source_file_id=row["source_file_id"],
132
+ target_file_id=row["target_file_id"],
133
+ module_name=row["module_name"],
134
+ )
135
+ for row in rows
136
+ ]
137
+
138
+ def get_symbol_imports(self, file_id: int) -> list[StoredSymbolImport]:
139
+ """Get all symbol-level imports for a file."""
140
+ rows = self.conn.execute(
141
+ "SELECT * FROM symbol_imports WHERE source_file_id = ?", (file_id,)
142
+ ).fetchall()
143
+
144
+ return [
145
+ StoredSymbolImport(
146
+ id=row["id"],
147
+ source_file_id=row["source_file_id"],
148
+ module_name=row["module_name"],
149
+ symbol_names=json.loads(row["symbol_names"]),
150
+ import_type=row["import_type"],
151
+ alias=row["alias"],
152
+ line=row["line"],
153
+ )
154
+ for row in rows
155
+ ]
156
+
157
+ # --- Call Storage ---
158
+
159
+ def store_call(
160
+ self,
161
+ call: ExtractedCall,
162
+ caller_symbol_id: str,
163
+ callee_symbol_id: Optional[str] = None,
164
+ ) -> None:
165
+ """Store a call relationship."""
166
+ # Determine call type
167
+ if call.receiver:
168
+ call_type = "method"
169
+ else:
170
+ call_type = "function"
171
+
172
+ self.conn.execute(
173
+ """
174
+ INSERT INTO calls (caller_id, callee_id, callee_name, call_type, receiver, line)
175
+ VALUES (?, ?, ?, ?, ?, ?)
176
+ """,
177
+ (
178
+ caller_symbol_id,
179
+ callee_symbol_id,
180
+ call.name,
181
+ call_type,
182
+ call.receiver,
183
+ call.line,
184
+ ),
185
+ )
186
+
187
+ def delete_caller_calls(self, caller_id: str) -> int:
188
+ """Delete all calls from a specific caller symbol."""
189
+ cursor = self.conn.execute(
190
+ "DELETE FROM calls WHERE caller_id = ?", (caller_id,)
191
+ )
192
+ return cursor.rowcount
193
+
194
+ def delete_file_calls(self, file_id: int) -> int:
195
+ """Delete all calls from symbols in a file."""
196
+ cursor = self.conn.execute(
197
+ """
198
+ DELETE FROM calls WHERE caller_id IN (
199
+ SELECT id FROM symbols WHERE file_id = ?
200
+ )
201
+ """,
202
+ (file_id,),
203
+ )
204
+ return cursor.rowcount
205
+
206
+ def get_calls_from(self, caller_id: str) -> list[StoredCall]:
207
+ """Get all calls made by a symbol."""
208
+ rows = self.conn.execute(
209
+ "SELECT * FROM calls WHERE caller_id = ?", (caller_id,)
210
+ ).fetchall()
211
+
212
+ return [self._row_to_call(row) for row in rows]
213
+
214
+ def get_calls_to(self, callee_id: str) -> list[StoredCall]:
215
+ """Get all calls to a symbol."""
216
+ rows = self.conn.execute(
217
+ "SELECT * FROM calls WHERE callee_id = ?", (callee_id,)
218
+ ).fetchall()
219
+
220
+ return [self._row_to_call(row) for row in rows]
221
+
222
+ def get_calls_by_name(self, name: str) -> list[StoredCall]:
223
+ """Get all calls to a function/method by name (including unresolved)."""
224
+ rows = self.conn.execute(
225
+ "SELECT * FROM calls WHERE callee_name = ?", (name,)
226
+ ).fetchall()
227
+
228
+ return [self._row_to_call(row) for row in rows]
229
+
230
+ def _row_to_call(self, row: sqlite3.Row) -> StoredCall:
231
+ """Convert a database row to a StoredCall."""
232
+ return StoredCall(
233
+ id=row["id"],
234
+ caller_id=row["caller_id"],
235
+ callee_id=row["callee_id"],
236
+ callee_name=row["callee_name"],
237
+ call_type=row["call_type"],
238
+ receiver=row["receiver"],
239
+ line=row["line"],
240
+ )
241
+
242
+ # --- Inheritance Storage ---
243
+
244
+ def store_inheritance(
245
+ self,
246
+ child_symbol_id: str,
247
+ cls: ExtractedClass,
248
+ parent_symbol_ids: Optional[dict[str, str]] = None,
249
+ ) -> None:
250
+ """Store inheritance relationships for a class."""
251
+ parent_symbol_ids = parent_symbol_ids or {}
252
+
253
+ for base_name in cls.bases:
254
+ parent_id = parent_symbol_ids.get(base_name)
255
+
256
+ self.conn.execute(
257
+ """
258
+ INSERT INTO inheritance (child_id, parent_name, parent_id, type)
259
+ VALUES (?, ?, ?, ?)
260
+ """,
261
+ (
262
+ child_symbol_id,
263
+ base_name,
264
+ parent_id,
265
+ "extends", # Python only has extends, not implements
266
+ ),
267
+ )
268
+
269
+ def delete_child_inheritance(self, child_id: str) -> int:
270
+ """Delete all inheritance relationships for a child class."""
271
+ cursor = self.conn.execute(
272
+ "DELETE FROM inheritance WHERE child_id = ?", (child_id,)
273
+ )
274
+ return cursor.rowcount
275
+
276
+ def delete_file_inheritance(self, file_id: int) -> int:
277
+ """Delete all inheritance relationships for classes in a file."""
278
+ cursor = self.conn.execute(
279
+ """
280
+ DELETE FROM inheritance WHERE child_id IN (
281
+ SELECT id FROM symbols WHERE file_id = ? AND kind = 'class'
282
+ )
283
+ """,
284
+ (file_id,),
285
+ )
286
+ return cursor.rowcount
287
+
288
+ def get_class_parents(self, child_id: str) -> list[StoredInheritance]:
289
+ """Get all parent classes for a class."""
290
+ rows = self.conn.execute(
291
+ "SELECT * FROM inheritance WHERE child_id = ?", (child_id,)
292
+ ).fetchall()
293
+
294
+ return [self._row_to_inheritance(row) for row in rows]
295
+
296
+ def get_class_children(self, parent_id: str) -> list[StoredInheritance]:
297
+ """Get all child classes for a class."""
298
+ rows = self.conn.execute(
299
+ "SELECT * FROM inheritance WHERE parent_id = ?", (parent_id,)
300
+ ).fetchall()
301
+
302
+ return [self._row_to_inheritance(row) for row in rows]
303
+
304
+ def _row_to_inheritance(self, row: sqlite3.Row) -> StoredInheritance:
305
+ """Convert a database row to a StoredInheritance."""
306
+ return StoredInheritance(
307
+ id=row["id"],
308
+ child_id=row["child_id"],
309
+ parent_name=row["parent_name"],
310
+ parent_id=row["parent_id"],
311
+ type=row["type"],
312
+ )
313
+
314
+ # --- Bulk Operations ---
315
+
316
+ def delete_all_file_relationships(self, file_id: int) -> dict[str, int]:
317
+ """Delete all relationships for a file. Returns counts by type."""
318
+ return {
319
+ "imports": self.delete_file_imports(file_id),
320
+ "calls": self.delete_file_calls(file_id),
321
+ "inheritance": self.delete_file_inheritance(file_id),
322
+ }
@@ -0,0 +1,154 @@
1
+ """
2
+ Database schema for local code intelligence storage.
3
+ """
4
+
5
+ import sqlite3
6
+
7
+ SCHEMA_VERSION = 2 # Increment when schema changes
8
+
9
+
10
+ def create_schema(conn: sqlite3.Connection) -> None:
11
+ """Create all database tables for code intelligence storage."""
12
+ conn.executescript(
13
+ """
14
+ -- Schema version tracking
15
+ CREATE TABLE IF NOT EXISTS schema_version (
16
+ version INTEGER PRIMARY KEY
17
+ );
18
+
19
+ -- Files table (already exists in watcher/storage.py, but included for completeness)
20
+ CREATE TABLE IF NOT EXISTS files (
21
+ id INTEGER PRIMARY KEY,
22
+ path TEXT UNIQUE NOT NULL,
23
+ content_hash TEXT NOT NULL,
24
+ last_modified REAL NOT NULL,
25
+ synced_at REAL,
26
+ status TEXT NOT NULL DEFAULT 'active'
27
+ );
28
+
29
+ -- Sync events table (already exists)
30
+ CREATE TABLE IF NOT EXISTS sync_events (
31
+ id INTEGER PRIMARY KEY,
32
+ file_id INTEGER NOT NULL,
33
+ event_type TEXT NOT NULL,
34
+ content_hash TEXT NOT NULL,
35
+ timestamp REAL NOT NULL,
36
+ FOREIGN KEY (file_id) REFERENCES files(id)
37
+ );
38
+
39
+ -- Symbols table (unified for all types: class, function, method, variable)
40
+ CREATE TABLE IF NOT EXISTS symbols (
41
+ id TEXT PRIMARY KEY, -- "path/file.py::ClassName::method"
42
+ name TEXT NOT NULL,
43
+ file_id INTEGER NOT NULL,
44
+ kind TEXT NOT NULL, -- class/function/method/variable/etc
45
+ parent_symbol_id TEXT, -- For methods: their class symbol id
46
+ exported INTEGER DEFAULT 0,
47
+ line_start INTEGER,
48
+ line_end INTEGER,
49
+ signature TEXT,
50
+ docstring TEXT,
51
+ extra_data TEXT, -- JSON for decorators, params, etc
52
+ FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
53
+ FOREIGN KEY (parent_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE
54
+ );
55
+
56
+ -- File-level imports (which files import which)
57
+ CREATE TABLE IF NOT EXISTS imports (
58
+ id INTEGER PRIMARY KEY,
59
+ source_file_id INTEGER NOT NULL,
60
+ target_file_id INTEGER, -- NULL if external module
61
+ module_name TEXT NOT NULL, -- The imported module path
62
+ UNIQUE(source_file_id, module_name),
63
+ FOREIGN KEY (source_file_id) REFERENCES files(id) ON DELETE CASCADE,
64
+ FOREIGN KEY (target_file_id) REFERENCES files(id) ON DELETE CASCADE
65
+ );
66
+
67
+ -- Symbol-level imports (what symbols are imported from where)
68
+ CREATE TABLE IF NOT EXISTS symbol_imports (
69
+ id INTEGER PRIMARY KEY,
70
+ source_file_id INTEGER NOT NULL,
71
+ module_name TEXT NOT NULL,
72
+ symbol_names TEXT NOT NULL, -- JSON array of imported symbol names
73
+ import_type TEXT NOT NULL, -- 'named', 'default', 'wildcard', 'namespace'
74
+ alias TEXT, -- Alias if renamed
75
+ line INTEGER,
76
+ FOREIGN KEY (source_file_id) REFERENCES files(id) ON DELETE CASCADE
77
+ );
78
+
79
+ -- Call edges (function/method calls)
80
+ CREATE TABLE IF NOT EXISTS calls (
81
+ id INTEGER PRIMARY KEY,
82
+ caller_id TEXT NOT NULL, -- Symbol id of the caller
83
+ callee_id TEXT, -- Symbol id of callee (NULL if unresolved)
84
+ callee_name TEXT NOT NULL, -- Name of the called function/method
85
+ call_type TEXT NOT NULL, -- 'function', 'method', 'constructor'
86
+ receiver TEXT, -- For method calls: the receiver expression
87
+ line INTEGER,
88
+ FOREIGN KEY (caller_id) REFERENCES symbols(id) ON DELETE CASCADE,
89
+ FOREIGN KEY (callee_id) REFERENCES symbols(id) ON DELETE SET NULL
90
+ );
91
+
92
+ -- Inheritance relationships
93
+ CREATE TABLE IF NOT EXISTS inheritance (
94
+ id INTEGER PRIMARY KEY,
95
+ child_id TEXT NOT NULL, -- Symbol id of the child class
96
+ parent_name TEXT NOT NULL, -- Name of the parent (may be unresolved)
97
+ parent_id TEXT, -- Symbol id of parent (NULL if external)
98
+ type TEXT NOT NULL, -- 'extends' or 'implements'
99
+ FOREIGN KEY (child_id) REFERENCES symbols(id) ON DELETE CASCADE,
100
+ FOREIGN KEY (parent_id) REFERENCES symbols(id) ON DELETE SET NULL
101
+ );
102
+
103
+ -- Indexes for efficient querying
104
+ CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
105
+ CREATE INDEX IF NOT EXISTS idx_files_status ON files(status);
106
+ CREATE INDEX IF NOT EXISTS idx_symbols_file_id ON symbols(file_id);
107
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
108
+ CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
109
+ CREATE INDEX IF NOT EXISTS idx_symbols_parent ON symbols(parent_symbol_id);
110
+ CREATE INDEX IF NOT EXISTS idx_imports_source ON imports(source_file_id);
111
+ CREATE INDEX IF NOT EXISTS idx_imports_target ON imports(target_file_id);
112
+ CREATE INDEX IF NOT EXISTS idx_symbol_imports_source ON symbol_imports(source_file_id);
113
+ CREATE INDEX IF NOT EXISTS idx_calls_caller ON calls(caller_id);
114
+ CREATE INDEX IF NOT EXISTS idx_calls_callee ON calls(callee_id);
115
+ CREATE INDEX IF NOT EXISTS idx_calls_name ON calls(callee_name);
116
+ CREATE INDEX IF NOT EXISTS idx_inheritance_child ON inheritance(child_id);
117
+ CREATE INDEX IF NOT EXISTS idx_inheritance_parent ON inheritance(parent_id);
118
+ """
119
+ )
120
+
121
+
122
+ def get_schema_version(conn: sqlite3.Connection) -> int:
123
+ """Get the current schema version from database."""
124
+ try:
125
+ cursor = conn.execute("SELECT version FROM schema_version LIMIT 1")
126
+ row = cursor.fetchone()
127
+ return row[0] if row else 0
128
+ except sqlite3.OperationalError:
129
+ return 0
130
+
131
+
132
+ def set_schema_version(conn: sqlite3.Connection, version: int) -> None:
133
+ """Set the schema version in database."""
134
+ conn.execute("DELETE FROM schema_version")
135
+ conn.execute("INSERT INTO schema_version (version) VALUES (?)", (version,))
136
+ conn.commit()
137
+
138
+
139
+ def migrate_schema(conn: sqlite3.Connection) -> bool:
140
+ """
141
+ Migrate schema to latest version if needed.
142
+ Returns True if migration was performed.
143
+ """
144
+ current_version = get_schema_version(conn)
145
+
146
+ if current_version >= SCHEMA_VERSION:
147
+ return False
148
+
149
+ # For now, we recreate schema on version mismatch
150
+ # In production, you'd want proper migrations
151
+ create_schema(conn)
152
+ set_schema_version(conn, SCHEMA_VERSION)
153
+
154
+ return True