Skip to content

Commit fc59245

Browse files
committed
Improve performance with crossplane by passing files directly where possible #89
1 parent 6862073 commit fc59245

File tree

7 files changed

+155
-10
lines changed

7 files changed

+155
-10
lines changed

.cursor/rules/12-python.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
description: Python interpreter and tooling (Always)
33
globs:
4-
alwaysApply: true
4+
alwaysApply: true
55
---
66

77
- Use the Python interpreter at `${env:HOME}/.virtualenvs/gixy/bin/python` for all Python execution in this workspace.

gixy/core/manager.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ def audit(self, file_path, file_data, is_stdin=False):
3030
parser = NginxParser(
3131
cwd=os.path.dirname(file_path) if not is_stdin else '',
3232
allow_includes=self.config.allow_includes)
33-
self.root = parser.parse(content=file_data.read(), path_info=file_path)
33+
if is_stdin:
34+
# Route stdin through parse_string for consistent path-based parsing via tempfile
35+
self.root = parser.parse_string(content=file_data.read(), path_info=file_path)
36+
else:
37+
# Prefer path-based parsing to avoid temporary files
38+
self.root = parser.parse_file(file_path)
3439

3540
push_context(self.root)
3641
self._audit_recursive(self.root.children)

gixy/parser/nginx_parser.py

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,128 @@ def __init__(self, cwd="", allow_includes=True):
2323
self._init_directives()
2424
self._path_stack = None
2525

26-
def parse_file(self, path, root=None):
27-
LOG.debug("Parse file: {0}".format(path))
28-
content = open(path).read()
29-
return self.parse(content=content, root=root, path_info=path)
26+
def parse_file(self, path, root=None, display_path=None):
27+
"""Parse an nginx configuration file from disk.
28+
29+
Args:
30+
path (str): Filesystem path to the nginx config to parse.
31+
root (Optional[block.Root]): Existing AST root to append into. If None, a new root is created.
32+
display_path (Optional[str]): Path to attribute to parsed nodes (used for stdin/tempfile attribution).
33+
34+
Returns:
35+
block.Root: Parsed configuration tree.
36+
37+
Raises:
38+
InvalidConfiguration: When parsing fails.
39+
"""
40+
LOG.debug("Parse file: {0}".format(display_path if display_path else path))
41+
root = self._ensure_root(root)
42+
try:
43+
parsed = self.parser.parse_path(path)
44+
except ParseException as e:
45+
error_msg = "char {char} (line:{line}, col:{col})".format(
46+
char=e.loc, line=e.lineno, col=e.col
47+
)
48+
LOG.error(
49+
'Failed to parse config "{file}": {error}'.format(
50+
file=path, error=error_msg
51+
)
52+
)
53+
raise InvalidConfiguration(error_msg)
54+
55+
current_path = display_path if display_path else path
56+
return self._build_tree_from_parsed(parsed, root, current_path)
57+
58+
def parse_string(self, content, root=None, path_info=None):
59+
"""Parse nginx configuration provided as a string/bytes.
60+
61+
The content is written to a temporary file so that the underlying
62+
crossplane parser consistently receives a filesystem path (ensuring
63+
identical behavior to file-based parsing).
64+
65+
Args:
66+
content (Union[str, bytes]): Nginx configuration text to parse.
67+
root (Optional[block.Root]): Existing AST root to append into. If None, a new root is created.
68+
path_info (Optional[str]): Path to attribute to parsed nodes (e.g., "<stdin>").
69+
70+
Returns:
71+
block.Root: Parsed configuration tree.
72+
73+
Raises:
74+
InvalidConfiguration: When parsing fails.
75+
"""
76+
root = self._ensure_root(root)
77+
import tempfile
78+
import os
79+
data = content if isinstance(content, (bytes, bytearray)) else content.encode('utf-8')
80+
tmp_filename = None
81+
try:
82+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.conf', delete=False) as tmp:
83+
tmp.write(data)
84+
tmp_filename = tmp.name
85+
return self.parse_file(tmp_filename, root=root, display_path=path_info)
86+
except ParseException as e:
87+
error_msg = "char {char} (line:{line}, col:{col})".format(
88+
char=e.loc, line=e.lineno, col=e.col
89+
)
90+
if path_info:
91+
LOG.error(
92+
'Failed to parse config "{file}": {error}'.format(
93+
file=path_info, error=error_msg
94+
)
95+
)
96+
else:
97+
LOG.error("Failed to parse config: {error}".format(error=error_msg))
98+
raise InvalidConfiguration(error_msg)
99+
finally:
100+
if tmp_filename:
101+
try:
102+
os.unlink(tmp_filename)
103+
except Exception:
104+
pass
105+
106+
# Backward-compatible alias (deprecated). Prefer parse_string.
107+
def parse(self, content, root=None, path_info=None):
108+
return self.parse_string(content, root=root, path_info=path_info)
109+
110+
def _ensure_root(self, root):
111+
"""Return provided root or create a new one.
112+
113+
Args:
114+
root (Optional[block.Root]): Existing root node or None.
115+
116+
Returns:
117+
block.Root: Root node.
118+
"""
119+
return root if root else block.Root()
120+
121+
def _build_tree_from_parsed(self, parsed_block, root, current_path):
122+
"""Finalize parsed data into the directive tree.
123+
124+
Handles nginx -T dumps, manages current file attribution, and
125+
appends parsed directives into the provided root.
126+
127+
Args:
128+
parsed_block (list): Parsed representation from RawParser.
129+
root (block.Root): Root node to append into.
130+
current_path (str): Current file path used for attribution.
131+
132+
Returns:
133+
block.Root: The root containing parsed directives.
134+
"""
135+
# Handle nginx -T dump format if detected (multi-file with file delimiters)
136+
if len(parsed_block) and parsed_block[0].getName() == "file_delimiter":
137+
LOG.info("Switched to parse nginx configuration dump.")
138+
root_filename = self._prepare_dump(parsed_block)
139+
self.is_dump = True
140+
self.cwd = os.path.dirname(root_filename)
141+
parsed_block = self.configs[root_filename]
142+
143+
# Parse into the provided root/parent context and keep attribution
144+
self._path_stack = current_path
145+
self.parse_block(parsed_block, root)
146+
self._path_stack = current_path
147+
return root
30148

31149
def parse(self, content, root=None, path_info=None):
32150
if path_info is not None:

gixy/parser/raw_parser.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,28 @@ def parse(self, data):
123123
except Exception as e:
124124
raise ParseException(str(e), loc=0, lineno=1, col=1)
125125

126+
def parse_path(self, path):
127+
"""
128+
Parse nginx configuration by file path using crossplane and convert
129+
the result into the legacy ParseResults structure.
130+
"""
131+
try:
132+
parsed = crossplane.parse(
133+
path,
134+
single=True,
135+
strict=False, # Allow directives outside their normal context
136+
check_ctx=False, # Skip context validation
137+
check_args=False, # Skip argument validation
138+
comments=True, # Include comments in the output
139+
)
140+
141+
return self._convert_crossplane_to_parseresults(parsed)
142+
except NgxParserBaseException as e:
143+
# Convert crossplane error to ParseException format
144+
raise ParseException(str(e), loc=0, lineno=getattr(e, 'line', 1), col=1)
145+
except Exception as e:
146+
raise ParseException(str(e), loc=0, lineno=1, col=1)
147+
126148
def _convert_crossplane_to_parseresults(self, crossplane_data):
127149
"""
128150
Convert crossplane's JSON format to the ParseResults format expected by the old parser.

tests/directives/test_block.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def _get_parsed(config):
8-
root = NginxParser(cwd='', allow_includes=False).parse(config)
8+
root = NginxParser(cwd='', allow_includes=False).parse_string(config)
99
return root.children[0]
1010

1111

@@ -147,7 +147,7 @@ def test_if_regex_backrefs_provide_variables():
147147
}
148148
"""
149149

150-
tree = NginxParser(cwd='', allow_includes=False).parse(config)
150+
tree = NginxParser(cwd='', allow_includes=False).parse_string(config)
151151
# Find the IfBlock and ensure variables (numeric backrefs) are provided
152152
if_block = None
153153
for child in tree.children:

tests/directives/test_directive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
def _get_parsed(config):
6-
root = NginxParser(cwd='', allow_includes=False).parse(config)
6+
root = NginxParser(cwd='', allow_includes=False).parse_string(config)
77
return root.children[0]
88

99

tests/parser/test_nginx_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def _parse(config):
8-
return NginxParser(cwd='', allow_includes=False).parse(config)
8+
return NginxParser(cwd='', allow_includes=False).parse_string(config)
99

1010

1111
@pytest.mark.parametrize('config,expected', zip(

0 commit comments

Comments
 (0)