tree-sitter-analyzer 1.7.1__py3-none-any.whl → 1.7.3__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 tree-sitter-analyzer might be problematic. Click here for more details.

@@ -11,7 +11,7 @@ Architecture:
11
11
  - Data Models: Generic and language-specific code element representations
12
12
  """
13
13
 
14
- __version__ = "1.7.1"
14
+ __version__ = "1.7.3"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -15,6 +15,7 @@ from ...constants import (
15
15
  get_element_type,
16
16
  )
17
17
  from ...output_manager import output_data, output_json, output_section
18
+ from ...formatters.language_formatter_factory import create_language_formatter
18
19
  from .base_command import BaseCommand
19
20
 
20
21
  if TYPE_CHECKING:
@@ -158,6 +159,16 @@ class AdvancedCommand(BaseCommand):
158
159
 
159
160
  def _output_full_analysis(self, analysis_result: "AnalysisResult") -> None:
160
161
  """Output full analysis results."""
162
+ # Check if we have a language-specific formatter
163
+ formatter = create_language_formatter(analysis_result.language)
164
+ if formatter:
165
+ # Use language-specific formatter
166
+ output_format = getattr(self.args, 'output_format', 'json')
167
+ formatted_output = formatter.format_advanced(self._convert_to_formatter_format(analysis_result), output_format)
168
+ print(formatted_output)
169
+ return
170
+
171
+ # Fallback to original implementation for unsupported languages
161
172
  output_section("Advanced Analysis Results")
162
173
  if self.args.output_format == "json":
163
174
  result_dict = {
@@ -182,6 +193,47 @@ class AdvancedCommand(BaseCommand):
182
193
  else:
183
194
  self._output_text_analysis(analysis_result)
184
195
 
196
+ def _convert_to_formatter_format(self, analysis_result: "AnalysisResult") -> dict:
197
+ """Convert AnalysisResult to format expected by formatters."""
198
+ return {
199
+ "file_path": analysis_result.file_path,
200
+ "language": analysis_result.language,
201
+ "line_count": analysis_result.line_count,
202
+ "element_count": len(analysis_result.elements),
203
+ "node_count": getattr(analysis_result, "node_count", 0),
204
+ "elements": [
205
+ {
206
+ "name": getattr(element, "name", str(element)),
207
+ "type": get_element_type(element),
208
+ "start_line": getattr(element, "start_line", 0),
209
+ "end_line": getattr(element, "end_line", 0),
210
+ "text": getattr(element, "text", ""),
211
+ "level": getattr(element, "level", 1),
212
+ "url": getattr(element, "url", ""),
213
+ "alt": getattr(element, "alt", ""),
214
+ "language": getattr(element, "language", ""),
215
+ "line_count": getattr(element, "line_count", 0),
216
+ "list_type": getattr(element, "list_type", ""),
217
+ "item_count": getattr(element, "item_count", 0),
218
+ "column_count": getattr(element, "column_count", 0),
219
+ "row_count": getattr(element, "row_count", 0),
220
+ "line_range": {
221
+ "start": getattr(element, "start_line", 0),
222
+ "end": getattr(element, "end_line", 0),
223
+ }
224
+ }
225
+ for element in analysis_result.elements
226
+ ],
227
+ "success": getattr(analysis_result, "success", True),
228
+ "analysis_time": getattr(analysis_result, "analysis_time", 0.0),
229
+ "analysis_metadata": {
230
+ "analysis_time": getattr(analysis_result, "analysis_time", 0.0),
231
+ "language": analysis_result.language,
232
+ "file_path": analysis_result.file_path,
233
+ "analyzer_version": "2.0.0",
234
+ }
235
+ }
236
+
185
237
  def _output_text_analysis(self, analysis_result: "AnalysisResult") -> None:
186
238
  """Output analysis in text format."""
187
239
  output_data(f"File: {analysis_result.file_path}")
@@ -16,6 +16,7 @@ from ...constants import (
16
16
  is_element_of_type,
17
17
  )
18
18
  from ...output_manager import output_data, output_json, output_section
19
+ from ...formatters.language_formatter_factory import create_language_formatter
19
20
  from .base_command import BaseCommand
20
21
 
21
22
  if TYPE_CHECKING:
@@ -34,7 +35,16 @@ class StructureCommand(BaseCommand):
34
35
  return 0
35
36
 
36
37
  def _output_structure_analysis(self, analysis_result: "AnalysisResult") -> None:
37
- """Output structure analysis results with appropriate Japanese header."""
38
+ """Output structure analysis results with appropriate header."""
39
+ # Check if we have a language-specific formatter
40
+ formatter = create_language_formatter(analysis_result.language)
41
+ if formatter:
42
+ # Use language-specific formatter
43
+ formatted_output = formatter.format_structure(self._convert_to_formatter_format(analysis_result))
44
+ print(formatted_output)
45
+ return
46
+
47
+ # Fallback to original implementation for unsupported languages
38
48
  output_section("Structure Analysis Results")
39
49
 
40
50
  # Convert to legacy structure format expected by tests
@@ -45,6 +55,45 @@ class StructureCommand(BaseCommand):
45
55
  else:
46
56
  self._output_text_format(structure_dict)
47
57
 
58
+ def _convert_to_formatter_format(self, analysis_result: "AnalysisResult") -> dict:
59
+ """Convert AnalysisResult to format expected by formatters."""
60
+ from ...constants import get_element_type
61
+
62
+ return {
63
+ "file_path": analysis_result.file_path,
64
+ "language": analysis_result.language,
65
+ "line_count": analysis_result.line_count,
66
+ "elements": [
67
+ {
68
+ "name": getattr(element, "name", str(element)),
69
+ "type": get_element_type(element),
70
+ "start_line": getattr(element, "start_line", 0),
71
+ "end_line": getattr(element, "end_line", 0),
72
+ "text": getattr(element, "text", ""),
73
+ "level": getattr(element, "level", 1),
74
+ "url": getattr(element, "url", ""),
75
+ "alt": getattr(element, "alt", ""),
76
+ "language": getattr(element, "language", ""),
77
+ "line_count": getattr(element, "line_count", 0),
78
+ "list_type": getattr(element, "list_type", ""),
79
+ "item_count": getattr(element, "item_count", 0),
80
+ "column_count": getattr(element, "column_count", 0),
81
+ "row_count": getattr(element, "row_count", 0),
82
+ "line_range": {
83
+ "start": getattr(element, "start_line", 0),
84
+ "end": getattr(element, "end_line", 0),
85
+ }
86
+ }
87
+ for element in analysis_result.elements
88
+ ],
89
+ "analysis_metadata": {
90
+ "analysis_time": getattr(analysis_result, "analysis_time", 0.0),
91
+ "language": analysis_result.language,
92
+ "file_path": analysis_result.file_path,
93
+ "analyzer_version": "2.0.0",
94
+ }
95
+ }
96
+
48
97
  def _convert_to_legacy_format(self, analysis_result: "AnalysisResult") -> dict:
49
98
  """Convert AnalysisResult to legacy structure format expected by tests."""
50
99
  import time
@@ -15,6 +15,7 @@ from ...constants import (
15
15
  is_element_of_type,
16
16
  )
17
17
  from ...output_manager import output_data, output_json, output_section
18
+ from ...formatters.language_formatter_factory import create_language_formatter
18
19
  from .base_command import BaseCommand
19
20
 
20
21
  if TYPE_CHECKING:
@@ -34,6 +35,15 @@ class SummaryCommand(BaseCommand):
34
35
 
35
36
  def _output_summary_analysis(self, analysis_result: "AnalysisResult") -> None:
36
37
  """Output summary analysis results."""
38
+ # Check if we have a language-specific formatter
39
+ formatter = create_language_formatter(analysis_result.language)
40
+ if formatter:
41
+ # Use language-specific formatter
42
+ formatted_output = formatter.format_summary(self._convert_to_formatter_format(analysis_result))
43
+ print(formatted_output)
44
+ return
45
+
46
+ # Fallback to original implementation for unsupported languages
37
47
  output_section("Summary Results")
38
48
 
39
49
  # Get summary types from args (default: classes,methods)
@@ -96,6 +106,45 @@ class SummaryCommand(BaseCommand):
96
106
  else:
97
107
  self._output_text_format(summary_data, requested_types)
98
108
 
109
+ def _convert_to_formatter_format(self, analysis_result: "AnalysisResult") -> dict[str, Any]:
110
+ """Convert AnalysisResult to format expected by formatters."""
111
+ from ...constants import get_element_type
112
+
113
+ return {
114
+ "file_path": analysis_result.file_path,
115
+ "language": analysis_result.language,
116
+ "line_count": analysis_result.line_count,
117
+ "elements": [
118
+ {
119
+ "name": getattr(element, "name", str(element)),
120
+ "type": get_element_type(element),
121
+ "start_line": getattr(element, "start_line", 0),
122
+ "end_line": getattr(element, "end_line", 0),
123
+ "text": getattr(element, "text", ""),
124
+ "level": getattr(element, "level", 1),
125
+ "url": getattr(element, "url", ""),
126
+ "alt": getattr(element, "alt", ""),
127
+ "language": getattr(element, "language", ""),
128
+ "line_count": getattr(element, "line_count", 0),
129
+ "list_type": getattr(element, "list_type", ""),
130
+ "item_count": getattr(element, "item_count", 0),
131
+ "column_count": getattr(element, "column_count", 0),
132
+ "row_count": getattr(element, "row_count", 0),
133
+ "line_range": {
134
+ "start": getattr(element, "start_line", 0),
135
+ "end": getattr(element, "end_line", 0),
136
+ }
137
+ }
138
+ for element in analysis_result.elements
139
+ ],
140
+ "analysis_metadata": {
141
+ "analysis_time": getattr(analysis_result, "analysis_time", 0.0),
142
+ "language": analysis_result.language,
143
+ "file_path": analysis_result.file_path,
144
+ "analyzer_version": "2.0.0",
145
+ }
146
+ }
147
+
99
148
  def _output_text_format(self, summary_data: dict, requested_types: list) -> None:
100
149
  """Output summary in human-readable text format."""
101
150
  output_data(f"File: {summary_data['file_path']}")
@@ -18,6 +18,7 @@ from ...constants import (
18
18
  )
19
19
  from ...output_manager import output_error
20
20
  from ...table_formatter import create_table_formatter
21
+ from ...formatters.language_formatter_factory import create_language_formatter
21
22
  from .base_command import BaseCommand
22
23
 
23
24
 
@@ -32,6 +33,16 @@ class TableCommand(BaseCommand):
32
33
  if not analysis_result:
33
34
  return 1
34
35
 
36
+ # Check if we have a language-specific formatter
37
+ formatter = create_language_formatter(analysis_result.language)
38
+ if formatter:
39
+ # Use language-specific formatter
40
+ table_type = getattr(self.args, 'table', 'full')
41
+ formatted_output = formatter.format_table(self._convert_to_formatter_format(analysis_result), table_type)
42
+ self._output_table(formatted_output)
43
+ return 0
44
+
45
+ # Fallback to original implementation for unsupported languages
35
46
  # Convert analysis result to structure format
36
47
  structure_result = self._convert_to_structure_format(
37
48
  analysis_result, language
@@ -53,6 +64,43 @@ class TableCommand(BaseCommand):
53
64
  output_error(f"An error occurred during table format analysis: {e}")
54
65
  return 1
55
66
 
67
+ def _convert_to_formatter_format(self, analysis_result: Any) -> dict[str, Any]:
68
+ """Convert AnalysisResult to format expected by formatters."""
69
+ return {
70
+ "file_path": analysis_result.file_path,
71
+ "language": analysis_result.language,
72
+ "line_count": analysis_result.line_count,
73
+ "elements": [
74
+ {
75
+ "name": getattr(element, "name", str(element)),
76
+ "type": get_element_type(element),
77
+ "start_line": getattr(element, "start_line", 0),
78
+ "end_line": getattr(element, "end_line", 0),
79
+ "text": getattr(element, "text", ""),
80
+ "level": getattr(element, "level", 1),
81
+ "url": getattr(element, "url", ""),
82
+ "alt": getattr(element, "alt", ""),
83
+ "language": getattr(element, "language", ""),
84
+ "line_count": getattr(element, "line_count", 0),
85
+ "list_type": getattr(element, "list_type", ""),
86
+ "item_count": getattr(element, "item_count", 0),
87
+ "column_count": getattr(element, "column_count", 0),
88
+ "row_count": getattr(element, "row_count", 0),
89
+ "line_range": {
90
+ "start": getattr(element, "start_line", 0),
91
+ "end": getattr(element, "end_line", 0),
92
+ }
93
+ }
94
+ for element in analysis_result.elements
95
+ ],
96
+ "analysis_metadata": {
97
+ "analysis_time": getattr(analysis_result, "analysis_time", 0.0),
98
+ "language": analysis_result.language,
99
+ "file_path": analysis_result.file_path,
100
+ "analyzer_version": "2.0.0",
101
+ }
102
+ }
103
+
56
104
  def _convert_to_structure_format(
57
105
  self, analysis_result: Any, language: str
58
106
  ) -> dict[str, Any]:
@@ -80,9 +80,24 @@ class QueryService:
80
80
  f"Query '{query_key}' not found for language '{language}'"
81
81
  )
82
82
 
83
- # Execute tree-sitter query
84
- ts_query = language_obj.query(query_string)
85
- captures = ts_query.captures(tree.root_node)
83
+ # Execute tree-sitter query using new API with fallback
84
+ import tree_sitter
85
+ captures = []
86
+
87
+ # Try to create and execute the query
88
+ try:
89
+ ts_query = tree_sitter.Query(language_obj, query_string)
90
+
91
+ # Try to execute the query
92
+ captures = ts_query.captures(tree.root_node)
93
+
94
+ # If captures is empty or not in expected format, try manual fallback
95
+ if not captures or (isinstance(captures, list) and len(captures) == 0):
96
+ captures = self._manual_query_execution(tree.root_node, query_key, language)
97
+
98
+ except (AttributeError, Exception) as e:
99
+ # If query creation or execution fails, use manual fallback
100
+ captures = self._manual_query_execution(tree.root_node, query_key, language)
86
101
 
87
102
  # Process capture results
88
103
  results = []
@@ -91,12 +106,19 @@ class QueryService:
91
106
  for capture_name, nodes in captures.items():
92
107
  for node in nodes:
93
108
  results.append(self._create_result_dict(node, capture_name))
94
- else:
95
- # Old tree-sitter API returns list of tuples
109
+ elif isinstance(captures, list):
110
+ # Handle both old API (list of tuples) and manual execution (list of tuples)
96
111
  for capture in captures:
97
112
  if isinstance(capture, tuple) and len(capture) == 2:
98
113
  node, name = capture
99
114
  results.append(self._create_result_dict(node, name))
115
+ else:
116
+ # If captures is not in expected format, try manual fallback
117
+ manual_captures = self._manual_query_execution(tree.root_node, query_key, language)
118
+ for capture in manual_captures:
119
+ if isinstance(capture, tuple) and len(capture) == 2:
120
+ node, name = capture
121
+ results.append(self._create_result_dict(node, name))
100
122
 
101
123
  # Apply filters
102
124
  if filter_expression and results:
@@ -160,3 +182,121 @@ class QueryService:
160
182
  return query_loader.get_query_description(language, query_key)
161
183
  except Exception:
162
184
  return None
185
+
186
+ def _manual_query_execution(self, root_node: Any, query_key: str | None, language: str) -> list[tuple[Any, str]]:
187
+ """
188
+ Manual query execution fallback for tree-sitter 0.25.x compatibility
189
+
190
+ Args:
191
+ root_node: Root node of the parsed tree
192
+ query_key: Query key to execute (can be None for custom queries)
193
+ language: Programming language
194
+
195
+ Returns:
196
+ List of (node, capture_name) tuples
197
+ """
198
+ captures = []
199
+
200
+ def walk_tree(node):
201
+ """Walk the tree and find matching nodes"""
202
+ # If query_key is None, this is a custom query - try to match common patterns
203
+ if query_key is None:
204
+ # For custom queries, try to match common node types
205
+ if language == "java":
206
+ if node.type == "method_declaration":
207
+ captures.append((node, "method"))
208
+ elif node.type == "class_declaration":
209
+ captures.append((node, "class"))
210
+ elif node.type == "field_declaration":
211
+ captures.append((node, "field"))
212
+ elif language == "python":
213
+ if node.type == "function_definition":
214
+ captures.append((node, "function"))
215
+ elif node.type == "class_definition":
216
+ captures.append((node, "class"))
217
+ elif node.type in ["import_statement", "import_from_statement"]:
218
+ captures.append((node, "import"))
219
+ elif language in ["javascript", "typescript"]:
220
+ if node.type in ["function_declaration", "method_definition"]:
221
+ captures.append((node, "function"))
222
+ elif node.type == "class_declaration":
223
+ captures.append((node, "class"))
224
+
225
+ # Markdown-specific queries
226
+ elif language == "markdown":
227
+ if query_key == "headers" and node.type in ["atx_heading", "setext_heading"]:
228
+ captures.append((node, "headers"))
229
+ elif query_key == "code_blocks" and node.type in ["fenced_code_block", "indented_code_block"]:
230
+ captures.append((node, "code_blocks"))
231
+ elif query_key == "links" and node.type == "inline":
232
+ # リンクは inline ノード内のパターンとして検出
233
+ node_text = node.text.decode('utf-8', errors='replace') if hasattr(node, 'text') and node.text else ""
234
+ if '[' in node_text and '](' in node_text:
235
+ captures.append((node, "links"))
236
+ elif query_key == "images" and node.type == "inline":
237
+ # 画像は inline ノード内のパターンとして検出
238
+ node_text = node.text.decode('utf-8', errors='replace') if hasattr(node, 'text') and node.text else ""
239
+ if '![' in node_text and '](' in node_text:
240
+ captures.append((node, "images"))
241
+ elif query_key == "lists" and node.type in ["list", "list_item"]:
242
+ captures.append((node, "lists"))
243
+ elif query_key == "emphasis" and node.type == "inline":
244
+ # 強調は inline ノード内の * や ** パターンとして検出
245
+ node_text = node.text.decode('utf-8', errors='replace') if hasattr(node, 'text') and node.text else ""
246
+ if '*' in node_text or '_' in node_text:
247
+ captures.append((node, "emphasis"))
248
+ elif query_key == "blockquotes" and node.type == "block_quote":
249
+ captures.append((node, "blockquotes"))
250
+ elif query_key == "tables" and node.type == "pipe_table":
251
+ captures.append((node, "tables"))
252
+ elif query_key == "horizontal_rules" and node.type == "thematic_break":
253
+ captures.append((node, "horizontal_rules"))
254
+ elif query_key == "html_blocks" and node.type == "html_block":
255
+ captures.append((node, "html_blocks"))
256
+ elif query_key == "inline_html" and node.type == "html_tag":
257
+ captures.append((node, "inline_html"))
258
+ elif query_key == "inline_code" and node.type == "code_span":
259
+ captures.append((node, "inline_code"))
260
+ elif query_key == "text_content" and node.type in ["paragraph", "inline"]:
261
+ captures.append((node, "text_content"))
262
+ elif query_key == "all_elements" and node.type in [
263
+ "atx_heading", "setext_heading", "fenced_code_block", "indented_code_block",
264
+ "inline", "list", "list_item", "block_quote", "pipe_table",
265
+ "paragraph", "section"
266
+ ]:
267
+ captures.append((node, "all_elements"))
268
+
269
+ # Python-specific queries
270
+ elif language == "python":
271
+ if query_key in ["function", "functions"] and node.type == "function_definition":
272
+ captures.append((node, "function"))
273
+ elif query_key in ["class", "classes"] and node.type == "class_definition":
274
+ captures.append((node, "class"))
275
+ elif query_key in ["import", "imports"] and node.type in ["import_statement", "import_from_statement"]:
276
+ captures.append((node, "import"))
277
+
278
+ # JavaScript/TypeScript-specific queries
279
+ elif language in ["javascript", "typescript"]:
280
+ if query_key == "function" and node.type in ["function_declaration", "function_expression", "arrow_function"]:
281
+ captures.append((node, "function"))
282
+ elif query_key == "class" and node.type == "class_declaration":
283
+ captures.append((node, "class"))
284
+ elif query_key == "method" and node.type == "method_definition":
285
+ captures.append((node, "method"))
286
+
287
+ # Java-specific queries
288
+ elif language == "java":
289
+ if query_key in ["method", "methods"] and node.type == "method_declaration":
290
+ # Always use "method" as capture name for consistency
291
+ captures.append((node, "method"))
292
+ elif query_key in ["class", "classes"] and node.type == "class_declaration":
293
+ captures.append((node, "class"))
294
+ elif query_key == "field" and node.type == "field_declaration":
295
+ captures.append((node, "field"))
296
+
297
+ # Recursively process children
298
+ for child in node.children:
299
+ walk_tree(child)
300
+
301
+ walk_tree(root_node)
302
+ return captures
@@ -1,12 +1,39 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Base formatter for language-specific table formatting.
3
+ Base formatter for language-specific formatting.
4
4
  """
5
5
 
6
6
  import csv
7
7
  import io
8
8
  from abc import ABC, abstractmethod
9
- from typing import Any
9
+ from typing import Any, Dict
10
+
11
+
12
+ class BaseFormatter(ABC):
13
+ """Base class for language-specific formatters"""
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ @abstractmethod
19
+ def format_summary(self, analysis_result: Dict[str, Any]) -> str:
20
+ """Format summary output"""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def format_structure(self, analysis_result: Dict[str, Any]) -> str:
25
+ """Format structure analysis output"""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def format_advanced(self, analysis_result: Dict[str, Any], output_format: str = "json") -> str:
30
+ """Format advanced analysis output"""
31
+ pass
32
+
33
+ @abstractmethod
34
+ def format_table(self, analysis_result: Dict[str, Any], table_type: str = "full") -> str:
35
+ """Format table output"""
36
+ pass
10
37
 
11
38
 
12
39
  class BaseTableFormatter(ABC):
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Factory for creating language-specific formatters for different output types.
4
+ """
5
+
6
+ from typing import Dict, Type, Any
7
+ from .base_formatter import BaseFormatter
8
+ from .markdown_formatter import MarkdownFormatter
9
+
10
+
11
+ class LanguageFormatterFactory:
12
+ """Factory for creating language-specific formatters"""
13
+
14
+ _formatters: Dict[str, Type[BaseFormatter]] = {
15
+ "markdown": MarkdownFormatter,
16
+ "md": MarkdownFormatter, # Alias
17
+ }
18
+
19
+ @classmethod
20
+ def create_formatter(cls, language: str) -> BaseFormatter:
21
+ """
22
+ Create formatter for specified language
23
+
24
+ Args:
25
+ language: Programming language name
26
+
27
+ Returns:
28
+ Language-specific formatter
29
+ """
30
+ formatter_class = cls._formatters.get(language.lower())
31
+
32
+ if formatter_class is None:
33
+ # Return None for unsupported languages
34
+ return None
35
+
36
+ return formatter_class()
37
+
38
+ @classmethod
39
+ def register_formatter(cls, language: str, formatter_class: Type[BaseFormatter]) -> None:
40
+ """
41
+ Register new language formatter
42
+
43
+ Args:
44
+ language: Programming language name
45
+ formatter_class: Formatter class
46
+ """
47
+ cls._formatters[language.lower()] = formatter_class
48
+
49
+ @classmethod
50
+ def get_supported_languages(cls) -> list[str]:
51
+ """
52
+ Get list of supported languages
53
+
54
+ Returns:
55
+ List of supported languages
56
+ """
57
+ return list(cls._formatters.keys())
58
+
59
+ @classmethod
60
+ def supports_language(cls, language: str) -> bool:
61
+ """
62
+ Check if language is supported
63
+
64
+ Args:
65
+ language: Programming language name
66
+
67
+ Returns:
68
+ True if language is supported
69
+ """
70
+ return language.lower() in cls._formatters
71
+
72
+
73
+ def create_language_formatter(language: str) -> BaseFormatter:
74
+ """
75
+ Create language formatter (function for compatibility)
76
+
77
+ Args:
78
+ language: Programming language name
79
+
80
+ Returns:
81
+ Language formatter or None if not supported
82
+ """
83
+ return LanguageFormatterFactory.create_formatter(language)