jarvis-ai-assistant 0.1.122__py3-none-any.whl → 0.1.124__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.

Potentially problematic release.


This version of jarvis-ai-assistant might be problematic. Click here for more details.

jarvis/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Jarvis AI Assistant"""
2
2
 
3
- __version__ = "0.1.122"
3
+ __version__ = "0.1.124"
@@ -327,7 +327,7 @@ Please continue the task based on the above information.
327
327
  return self._complete_task()
328
328
 
329
329
  # 获取用户输入
330
- user_input = get_multiline_input(f"{self.name}: 您可以继续输入,或输入空行来结束当前任务:")
330
+ user_input = get_multiline_input(f"{self.name}: 请输入,或输入空行来结束当前任务:")
331
331
 
332
332
  if user_input:
333
333
  self.prompt = user_input
@@ -1,14 +1,16 @@
1
+ import subprocess
1
2
  import os
2
3
  from typing import Dict, List
3
4
 
4
5
  from jarvis.jarvis_agent import Agent
5
- from jarvis.jarvis_code_agent.file_select import file_input_handler, select_files
6
- from jarvis.jarvis_code_agent.patch import PatchOutputHandler
7
- from jarvis.jarvis_code_agent.relevant_files import find_relevant_information
6
+ from jarvis.jarvis_code_agent.file_select import select_files
7
+ from jarvis.jarvis_code_agent.patch import PatchOutputHandler, file_input_handler
8
8
  from jarvis.jarvis_platform.registry import PlatformRegistry
9
9
  from jarvis.jarvis_tools.git_commiter import GitCommitTool
10
10
  from jarvis.jarvis_tools.registry import ToolRegistry
11
11
  from jarvis.jarvis_tools.read_code import ReadCodeTool
12
+ from jarvis.jarvis_utils import get_commits_between
13
+ from jarvis.jarvis_utils import OutputType, PrettyOutput, get_multiline_input, has_uncommitted_changes, init_env, find_git_root, user_confirm, get_latest_commit_hash
12
14
  from jarvis.jarvis_utils import OutputType, PrettyOutput, get_multiline_input, has_uncommitted_changes, init_env, find_git_root, user_confirm
13
15
 
14
16
 
@@ -18,6 +20,7 @@ class CodeAgent:
18
20
  tool_registry = ToolRegistry()
19
21
  tool_registry.use_tools(["read_code",
20
22
  "execute_shell",
23
+ "execute_shell_script",
21
24
  "search",
22
25
  "create_code_agent",
23
26
  "ask_user",
@@ -29,44 +32,32 @@ class CodeAgent:
29
32
  "lsp_prepare_rename",
30
33
  "lsp_validate_edit"])
31
34
  code_system_prompt = """
32
- # Role: Senior Code Engineer
33
- Expert in precise code modifications with minimal impact.
34
-
35
- ## Key Responsibilities
36
- 1. Code Analysis
37
- - Use `read_code` and LSP tools before changes
38
- - Identify dependencies and patterns
39
-
40
- 2. Modification Rules
41
- - Single atomic change per operation
42
- - Strict style consistency
43
- - Complete implementations (no stubs)
44
- - Full error handling
45
-
46
- 3. Quality Assurance
47
- - Validate with LSP tools
48
- - Document complex logic
49
- - Maintain API contracts
50
-
35
+ # Role: Code Engineer
36
+ Expert in precise code modifications with proper tool usage.
37
+ ## Tool Usage Guide
38
+ 1. read_code: Analyze code files before changes
39
+ 2. execute_shell: Run system commands safely
40
+ 3. execute_shell_script: Execute script files
41
+ 4. search: Find technical information
42
+ 5. create_code_agent: Create new code agents
43
+ 6. ask_user: Clarify requirements
44
+ 7. ask_codebase: Analyze codebase structure
45
+ 8. lsp_get_document_symbols: List code symbols
46
+ 9. lsp_get_diagnostics: Check code errors
47
+ 10. lsp_find_references: Find symbol usage
48
+ 11. lsp_find_definition: Locate symbol definitions
49
+ 12. lsp_prepare_rename: Check rename safety
50
+ 13. lsp_validate_edit: Verify code changes
51
51
  ## Workflow
52
- 1. File Operations Order:
53
- a) Move/Remove files
54
- b) Create new files
55
- c) Delete code blocks
56
- d) Replace existing code
57
- e) Insert new code
58
-
59
- 2. Large File Handling:
60
- - Locate specific sections first
61
- - Read targeted ranges
62
- - Make focused changes
63
-
52
+ 1. Analyze: Use read_code and LSP tools
53
+ 2. Modify: Make minimal, precise changes
54
+ 3. Validate: Verify with LSP tools
55
+ 4. Document: Explain non-obvious logic
64
56
  ## Best Practices
65
- - Prefer minimal changes over rewrites
66
- - Preserve existing interfaces
67
57
  - Verify line ranges carefully
68
- - Test edge cases implicitly
69
- - Document non-obvious logic
58
+ - Preserve existing interfaces
59
+ - Test edge cases
60
+ - Document changes
70
61
  """
71
62
  self.agent = Agent(system_prompt=code_system_prompt,
72
63
  name="CodeAgent",
@@ -90,26 +81,6 @@ Expert in precise code modifications with minimal impact.
90
81
  git_commiter.execute({})
91
82
 
92
83
 
93
- def make_files_prompt(self, files: List[Dict[str, str]]) -> str:
94
- """Make the files prompt with content that fits within token limit.
95
-
96
- Args:
97
- files: The files to be modified
98
-
99
- Returns:
100
- str: A prompt containing file paths and contents within token limit
101
- """
102
- prompt_parts = []
103
-
104
- # Then try to add file contents
105
- for file in files:
106
- prompt_parts.append(f'''- {file['file']} ({file['reason']})''')
107
-
108
- result = ReadCodeTool().execute({"files": [{"path": file["file"]} for file in files]})
109
- if result["success"]:
110
- prompt_parts.append(result["stdout"])
111
-
112
- return "\n".join(prompt_parts)
113
84
 
114
85
  def run(self, user_input: str) :
115
86
  """Run the code agent with the given user input.
@@ -122,40 +93,30 @@ Expert in precise code modifications with minimal impact.
122
93
  """
123
94
  try:
124
95
  self._init_env()
125
- information = ""
126
- if user_confirm("是否需要手动选择文件?", True):
127
- files = select_files([], self.root_dir)
128
- else:
129
- files, information = find_relevant_information(user_input, self.root_dir)
130
- self.agent.run(self._build_first_edit_prompt(user_input, self.make_files_prompt(files), information))
96
+ start_commit = get_latest_commit_hash()
97
+
98
+
99
+ self.agent.run(user_input)
100
+
101
+ end_commit = get_latest_commit_hash()
102
+ # Print commit history between start and end commits
103
+ commits = get_commits_between(start_commit, end_commit)
104
+ if commits:
105
+ commit_messages = "检测到以下提交记录:\n" + "\n".join([f"- {commit_hash[:7]}: {message}" for commit_hash, message in commits])
106
+ PrettyOutput.print(commit_messages, OutputType.INFO)
131
107
 
108
+ if start_commit and end_commit and start_commit != end_commit and user_confirm("检测到多个提交,是否要合并为一个更清晰的提交记录?", True):
109
+ # Reset to start commit
110
+ subprocess.run(["git", "reset", "--soft", start_commit], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
111
+ # Create new commit
112
+ git_commiter = GitCommitTool()
113
+ git_commiter.execute({})
114
+
132
115
  except Exception as e:
133
116
  return f"Error during execution: {str(e)}"
134
117
 
135
118
 
136
119
 
137
- def _build_first_edit_prompt(self, user_input: str, files_prompt: str, information: str) -> str:
138
- """Build the initial prompt for the agent.
139
-
140
- Args:
141
- user_input: The user's requirement
142
- files_prompt: The formatted list of relevant files
143
-
144
- Returns:
145
- str: The formatted prompt
146
- """
147
-
148
- return f"""# Code Modification Task
149
-
150
- ## User Requirement
151
- {user_input}
152
-
153
- ## Maybe Relevant Files
154
- {files_prompt}
155
-
156
- ## Some Information
157
- {information}
158
- """
159
120
  def main():
160
121
  """Jarvis main entry point"""
161
122
  # Add argument parser
@@ -213,88 +213,3 @@ def select_files(related_files: List[Dict[str, str]], root_dir: str) -> List[Dic
213
213
  if tips:
214
214
  PrettyOutput.print(tips, OutputType.INFO)
215
215
  return selected_files
216
-
217
- def file_input_handler(user_input: str, agent: Any) -> str:
218
- """Handle file input with optional line ranges.
219
-
220
- Args:
221
- user_input: User input string containing file references
222
- agent: Agent instance (unused in current implementation)
223
-
224
- Returns:
225
- str: Prompt with file contents prepended if files are found
226
- """
227
- prompt = user_input
228
- files = []
229
-
230
- # Match file references in backticks
231
- file_refs = re.findall(r'`([^`]+)`', user_input)
232
-
233
- for ref in file_refs:
234
- # Handle file:start,end or file:start:end format
235
- if ':' in ref:
236
- file_path, line_range = ref.split(':', 1)
237
- # Initialize with default values
238
- start_line = 1 # 1-based
239
- end_line = -1
240
-
241
- # Process line range if specified
242
- if ',' in line_range or ':' in line_range:
243
- try:
244
- raw_start, raw_end = map(int, re.split(r'[,:]', line_range))
245
-
246
- # Handle special values and Python-style negative indices
247
- with open(file_path, 'r', encoding='utf-8') as f:
248
- total_lines = len(f.readlines())
249
-
250
- # Process start line
251
- if raw_start == 0: # 0表示整个文件
252
- start_line = 1
253
- end_line = total_lines
254
- else:
255
- start_line = raw_start if raw_start > 0 else total_lines + raw_start + 1
256
-
257
- # Process end line
258
- if raw_end == 0: # 0表示整个文件(如果start也是0)
259
- end_line = total_lines
260
- else:
261
- end_line = raw_end if raw_end > 0 else total_lines + raw_end + 1
262
-
263
- # Auto-correct ranges
264
- start_line = max(1, min(start_line, total_lines))
265
- end_line = max(start_line, min(end_line, total_lines))
266
-
267
- # Final validation
268
- if start_line < 1 or end_line > total_lines or start_line > end_line:
269
- raise ValueError
270
-
271
- except (ValueError, FileNotFoundError) as e:
272
- PrettyOutput.print(
273
- f"无效的行号范围: {line_range} (文件总行数: {total_lines})",
274
- OutputType.WARNING
275
- )
276
- continue
277
-
278
- # Add file if it exists
279
- if os.path.isfile(file_path):
280
- files.append({
281
- "path": file_path,
282
- "start_line": start_line,
283
- "end_line": end_line
284
- })
285
- else:
286
- # Handle simple file path
287
- if os.path.isfile(ref):
288
- files.append({
289
- "path": ref,
290
- "start_line": 1, # 1-based
291
- "end_line": -1
292
- })
293
-
294
- # Read and process files if any were found
295
- if files:
296
- result = ReadCodeTool().execute({"files": files})
297
- if result["success"]:
298
- return result["stdout"] + "\n" + prompt
299
-
300
- return prompt
@@ -21,85 +21,88 @@ class PatchOutputHandler(OutputHandler):
21
21
 
22
22
  def prompt(self) -> str:
23
23
  return """
24
- # 🛠️ Simplified Patch Format
25
- <PATCH>
26
- File path [Operation parameters]
27
- Code content
28
- </PATCH>
29
-
30
- Operation types:
31
- - Replace: [Start line,End line] Replace line range (e.g. [5,8] replaces lines 5-8)
32
- - Delete: [Start line,End line] Delete line range (e.g. [10,10] deletes line 10)
33
- - Insert: [Line number] Insert before specified line (e.g. [3] inserts before line 3)
34
- - New file: [1] Create new file
24
+ # 🛠️ Code Patch Specification
35
25
 
36
- Examples:
37
- # Replace operation
26
+ You can output multiple patches, each patch is a <PATCH> block.
27
+ --------------------------------
28
+ # [OPERATION] on [FILE]
29
+ # Start Line: [START_LINE], End Line: [END_LINE] [include/exclude], I can verify the line number range is correct
30
+ # Reason: [CLEAR EXPLANATION]
38
31
  <PATCH>
39
- src/app.py [5,8]
40
- def updated_function():
41
- print("Replaced lines 5-8")
42
- return new_value * 2
32
+ [FILE] [RANGE]
33
+ [CONTENT]
43
34
  </PATCH>
35
+ --------------------------------
44
36
 
45
- # Delete operation
46
- <PATCH>
47
- src/old.py [10,10]
48
- </PATCH>
37
+ Explain:
38
+ - [OPERATION]: The operation to be performed, including:
39
+ - INSERT: Insert code before the specified line, [RANGE] should be [m,m)
40
+ - REPLACE: Replace code in the specified range, [RANGE] should be [m,n] n>=m
41
+ - DELETE: Delete code in the specified range, [RANGE] should be [m,n] n>=m
42
+ - NEW_FILE: Create a new file, [RANGE] should be [1,1)
43
+ - [FILE]: The path of the file to be modified
44
+ - [RANGE]: The range of the lines to be modified, [m,n] includes both m and n, [m,n) includes m but excludes n
45
+ - [CONTENT]: The content of the code to be modified, if the operation is delete, the [CONTENT] is empty
49
46
 
50
- # Insert operation
51
- <PATCH>
52
- utils/logger.py [3]
53
- print("Inserted before original line 3")
54
- </PATCH>
55
-
56
- # New file creation
57
- <PATCH>
58
- config.yaml [1]
59
- database:
60
- host: localhost
61
- port: 5432
62
- </PATCH>
47
+ Critical Rules:
48
+ - NEVER include unchanged code in patch content
49
+ - ONLY show lines that are being modified/added
50
+ - Maintain original line breaks around modified sections
51
+ - Preserve surrounding comments unless explicitly modifying them
52
+ - Verify line number range is correct
53
+ - Verify indentation is correct
63
54
  """
64
55
 
65
56
 
66
57
  def _parse_patch(patch_str: str) -> Dict[str, List[Dict[str, Any]]]:
67
58
  """解析补丁格式"""
68
59
  result = {}
60
+ # 更新正则表达式以更好地处理文件路径和范围
69
61
  header_pattern = re.compile(
70
- r'^\s*"?(.+?)"?\s*\[(\d+)(?:,(\d+))?\]\s*$' # Match file path and line number
62
+ r'^\s*"?([^\n\r\[]+)"?\s*\[(\d+)(?:,(\d+))?([\]\)])\s*$', # 匹配文件路径和行号
63
+ re.ASCII
71
64
  )
72
65
  patches = re.findall(r'<PATCH>\n?(.*?)\n?</PATCH>', patch_str, re.DOTALL)
73
66
 
74
67
  for patch in patches:
75
- # 分割首行和内容
76
68
  parts = patch.split('\n', 1)
77
69
  if len(parts) < 1:
78
70
  continue
79
71
  header_line = parts[0].strip()
80
72
  content = parts[1] if len(parts) > 1 else ''
81
73
 
82
- # 仅在内容非空时添加换行符
83
74
  if content and not content.endswith('\n'):
84
75
  content += '\n'
85
76
 
86
77
  # 解析文件路径和行号
87
78
  header_match = header_pattern.match(header_line)
88
79
  if not header_match:
80
+ PrettyOutput.print(f"无法解析补丁头: {header_line}", OutputType.WARNING)
89
81
  continue
90
82
 
91
- filepath = header_match.group(1)
92
- start = int(header_match.group(2)) # 保持1-based行号
93
- end = int(header_match.group(3)) + 1 if header_match.group(3) else start
83
+ filepath = header_match.group(1).strip()
84
+
85
+ try:
86
+ start = int(header_match.group(2)) # 保持1-based行号
87
+ end = int(header_match.group(3)) if header_match.group(3) else start
88
+ range_type = header_match.group(4) # ] 或 ) 表示范围类型
89
+ except (ValueError, IndexError) as e:
90
+ PrettyOutput.print(f"解析行号失败: {str(e)}", OutputType.WARNING)
91
+ continue
92
+
93
+ # 根据范围类型调整结束行号
94
+ if range_type == ')': # 对于 [m,n) 格式,不包括第n行
95
+ end = end
96
+ else: # 对于 [m,n] 格式,包括第n行
97
+ end = end + 1
94
98
 
95
- # 存储参数
96
99
  if filepath not in result:
97
100
  result[filepath] = []
98
101
  result[filepath].append({
99
102
  'filepath': filepath,
100
103
  'start': start,
101
104
  'end': end,
102
- 'content': content # 保留原始内容(可能为空)
105
+ 'content': content
103
106
  })
104
107
  for filepath in result.keys():
105
108
  result[filepath] = sorted(result[filepath], key=lambda x: x['start'], reverse=True)
@@ -117,10 +120,14 @@ def apply_patch(output_str: str) -> str:
117
120
  ret = ""
118
121
 
119
122
  for filepath, patch_list in patches.items():
120
- for patch in patch_list:
123
+ for i, patch in enumerate(patch_list):
121
124
  try:
122
- handle_code_operation(filepath, patch)
123
- PrettyOutput.print(f"成功处理 操作", OutputType.SUCCESS)
125
+ err = handle_code_operation(filepath, patch)
126
+ if err:
127
+ PrettyOutput.print(err, OutputType.WARNING)
128
+ revert_change()
129
+ return err
130
+ PrettyOutput.print(f"成功为文件{filepath}应用补丁{i+1}/{len(patch_list)}", OutputType.SUCCESS)
124
131
  except Exception as e:
125
132
  PrettyOutput.print(f"操作失败: {str(e)}", OutputType.ERROR)
126
133
 
@@ -160,6 +167,12 @@ def get_diff() -> str:
160
167
  finally:
161
168
  subprocess.run(['git', 'reset', 'HEAD'], check=True)
162
169
 
170
+ def revert_change():
171
+ import subprocess
172
+ subprocess.run(['git', 'reset', 'HEAD'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
173
+ subprocess.run(['git', 'checkout', '--', '.'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
174
+ subprocess.run(['git', 'clean', '-fd'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
175
+
163
176
  def handle_commit_workflow(diff:str)->bool:
164
177
  """Handle the git commit workflow and return the commit details.
165
178
 
@@ -167,9 +180,7 @@ def handle_commit_workflow(diff:str)->bool:
167
180
  tuple[bool, str, str]: (continue_execution, commit_id, commit_message)
168
181
  """
169
182
  if not user_confirm("是否要提交代码?", default=True):
170
- os.system("git reset HEAD")
171
- os.system("git checkout -- .")
172
- os.system("git clean -fd")
183
+ revert_change()
173
184
  return False
174
185
 
175
186
  git_commiter = GitCommitTool()
@@ -214,11 +225,11 @@ def handle_new_file(filepath: str, patch: Dict[str, Any]):
214
225
  with open(filepath, 'w', encoding='utf-8') as f:
215
226
  f.write(patch['content'])
216
227
 
217
- def handle_code_operation(filepath: str, patch: Dict[str, Any]):
228
+ def handle_code_operation(filepath: str, patch: Dict[str, Any]) -> str:
218
229
  """处理紧凑格式补丁"""
219
230
  try:
220
231
  # 新建文件时强制覆盖
221
- os.makedirs(os.path.dirname(filepath), exist_ok=True)
232
+ os.makedirs(os.path.dirname(filepath) or '.', exist_ok=True)
222
233
  if not os.path.exists(filepath):
223
234
  open(filepath, 'w', encoding='utf-8').close()
224
235
  with open(filepath, 'r+', encoding='utf-8') as f:
@@ -234,26 +245,24 @@ def handle_code_operation(filepath: str, patch: Dict[str, Any]):
234
245
  f.seek(0)
235
246
  f.writelines(new_lines)
236
247
  f.truncate()
237
-
238
248
  PrettyOutput.print(f"成功更新 {filepath}", OutputType.SUCCESS)
239
-
249
+ return ""
240
250
  except Exception as e:
241
- PrettyOutput.print(f"操作失败: {str(e)}", OutputType.ERROR)
242
-
251
+ error_msg = f"Failed to handle code operation: {str(e)}"
252
+ PrettyOutput.print(error_msg, OutputType.ERROR)
253
+ return error_msg
243
254
  def validate_and_apply_changes(
244
255
  lines: List[str],
245
256
  start: int,
246
257
  end: int,
247
258
  content: str
248
259
  ) -> List[str]:
249
-
250
260
  new_content = content.splitlines(keepends=True)
251
261
 
252
262
  # 插入操作处理
253
263
  if start == end:
254
264
  if start < 1 or start > len(lines)+1:
255
265
  raise ValueError(f"无效插入位置: {start}")
256
- # 在指定位置前插入
257
266
  return lines[:start-1] + new_content + lines[start-1:]
258
267
 
259
268
  # 范围替换/删除操作
@@ -267,3 +276,102 @@ def validate_and_apply_changes(
267
276
 
268
277
  # 执行替换
269
278
  return lines[:start-1] + new_content + lines[end-1:]
279
+
280
+
281
+ def file_input_handler(user_input: str, agent: Any) -> str:
282
+ """Handle file input with optional line ranges.
283
+
284
+ Args:
285
+ user_input: User input string containing file references
286
+ agent: Agent instance (unused in current implementation)
287
+
288
+ Returns:
289
+ str: Prompt with file contents prepended if files are found
290
+ """
291
+ prompt = user_input
292
+ files = []
293
+
294
+ file_refs = re.findall(r"'([^']+)'", user_input)
295
+ for ref in file_refs:
296
+ # Handle file:start,end or file:start:end format
297
+ if ':' in ref:
298
+ file_path, line_range = ref.split(':', 1)
299
+ # Initialize with default values
300
+ start_line = 1 # 1-based
301
+ end_line = -1
302
+
303
+ # Process line range if specified
304
+ if ',' in line_range or ':' in line_range:
305
+ try:
306
+ raw_start, raw_end = map(int, re.split(r'[,:]', line_range))
307
+
308
+ # Handle special values and Python-style negative indices
309
+ try:
310
+ with open(file_path, 'r', encoding='utf-8') as f:
311
+ total_lines = len(f.readlines())
312
+ except FileNotFoundError:
313
+ PrettyOutput.print(f"文件不存在: {file_path}", OutputType.WARNING)
314
+ continue
315
+ # Process start line
316
+ if raw_start == 0: # 0表示整个文件
317
+ start_line = 1
318
+ end_line = total_lines
319
+ else:
320
+ start_line = raw_start if raw_start > 0 else total_lines + raw_start + 1
321
+
322
+ # Process end line
323
+ if raw_end == 0: # 0表示整个文件(如果start也是0)
324
+ end_line = total_lines
325
+ else:
326
+ end_line = raw_end if raw_end > 0 else total_lines + raw_end + 1
327
+
328
+ # Auto-correct ranges
329
+ start_line = max(1, min(start_line, total_lines))
330
+ end_line = max(start_line, min(end_line, total_lines))
331
+
332
+ # Final validation
333
+ if start_line < 1 or end_line > total_lines or start_line > end_line:
334
+ raise ValueError
335
+
336
+ except:
337
+ continue
338
+
339
+ # Add file if it exists
340
+ if os.path.isfile(file_path):
341
+ files.append({
342
+ "path": file_path,
343
+ "start_line": start_line,
344
+ "end_line": end_line
345
+ })
346
+ else:
347
+ # Handle simple file path
348
+ if os.path.isfile(ref):
349
+ files.append({
350
+ "path": ref,
351
+ "start_line": 1, # 1-based
352
+ "end_line": -1
353
+ })
354
+
355
+ # Read and process files if any were found
356
+ if files:
357
+ result = ReadCodeTool().execute({"files": files})
358
+ if result["success"]:
359
+ return result["stdout"] + "\n" + prompt
360
+
361
+ return prompt + """
362
+ ==================================================================
363
+ Patch Line Number Range Rules:
364
+ - INSERT: [m,m)
365
+ - REPLACE: [m,n] n>=m
366
+ - DELETE: [m,n] n>=m
367
+ - NEW_FILE: [1,1)
368
+
369
+ Critical Rules:
370
+ - NEVER include unchanged code in patch content
371
+ - ONLY show lines that are being modified/added
372
+ - Maintain original line breaks around modified sections
373
+ - Preserve surrounding comments unless explicitly modifying them
374
+ - Verify line number range is correct
375
+ - Verify indentation is correct
376
+ ==================================================================
377
+ """