From 7615867fa799dd34f1b9f2cf23423533c4c18bc6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=8Dcaro=20Botelho?=
<111321065+icrcode@users.noreply.github.com>
Date: Wed, 23 Apr 2025 16:11:55 -0300
Subject: [PATCH] Add export command and results exporting functionality
---
CODE_OF_CONDUCT.md | 12 +++
cli/commands/analyze.py | 22 +++--
cli/commands/export/__init__.py | 3 +
cli/commands/export/export.py | 120 +++++++++++++++++++++++++++
cli/main.py | 41 +++------
lexers/golang/golexer.py | 19 ++++-
lexers/javascript/javascriptlexer.py | 68 ++++++++++++++-
spice/analyze.py | 52 +++---------
8 files changed, 256 insertions(+), 81 deletions(-)
create mode 100644 cli/commands/export/__init__.py
create mode 100644 cli/commands/export/export.py
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 650f4b9..32a5a3b 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,14 +1,17 @@
# The Spice Code of Conduct
## **The Great Pledge**
+
As Fremen of this digital desert, we pledge to make our oasis of code a safe and welcoming place for all. Regardless of background, experience, or identity, all who walk the sands of Spice Code CLI shall find respect and camaraderie.
We shall act with honor, uphold fairness, and protect our community from the corruption of toxicity, so that our shared journey may be one of learning and mutual growth.
## **The Way of the Spice**
+
To ensure our collective strength, we adhere to these principles:
### **What is Expected of All Fremen**
+
- Show kindness, patience, and respect to fellow travelers.
- Honor differences in coding styles, ideas, and perspectives.
- Provide constructive feedback with humility and receive it with grace.
@@ -16,37 +19,46 @@ To ensure our collective strength, we adhere to these principles:
- Work for the greater good of the community, not just personal gain.
### **That Which is Forbidden**
+
- Harassment, discrimination, or hostility in any form.
- Trolling, insults, or personal attacks—only the weak resort to such things.
- Sharing private information without consent, for such betrayals are not easily forgiven.
- Any conduct unbecoming of a Fremen coder that disrupts the harmony of our sietch.
## **The Law of the Desert**
+
The stewards of Spice Code CLI (maintainers and community leaders) shall act as judges in matters of discord. They have the right to remove, edit, or reject any contribution that violates our principles.
## **The Fremen Enforcement Measures**
+
Those who stray from the path shall be met with fair but firm consequences:
### **1. A Whisper on the Wind**
+
_Impact_: A minor breach—unintended offense or misunderstanding.
_Consequence_: A private warning and guidance toward the proper path.
### **2. The Warning of the Sietch**
+
_Impact_: A more serious offense or repeated minor offenses.
_Consequence_: A public warning with temporary restrictions on participation.
### **3. Exile from the Oasis**
+
_Impact_: Disruptive or harmful behavior that endangers the community.
_Consequence_: A temporary ban from Spice Code CLI spaces.
### **4. Cast Into the Deep Desert**
+
_Impact_: Malicious intent, harassment, or repeated major offenses.
_Consequence_: Permanent banishment—no spice, no code, no return.
## **Reporting a Violation**
+
If you witness behavior unworthy of the Spice Code, report it to the stewards at spicecodecli@gmail.com. Reports will be handled swiftly, fairly, and with the utmost discretion.
## **Final Words**
+
Spice Code CLI exists to empower developers. Let us build with respect, learn from one another, and ensure our community thrives. The spice must flow, but toxicity shall not.
**“He who controls the code, controls the future.”**
diff --git a/cli/commands/analyze.py b/cli/commands/analyze.py
index cb47993..2ec16c4 100644
--- a/cli/commands/analyze.py
+++ b/cli/commands/analyze.py
@@ -12,18 +12,20 @@ def analyze_command(file, all, json_output, LANG_FILE):
# load translations
messages = get_translation(LANG_FILE)
- # define available stats UPDATE THIS WHEN NEEDED PLEASE !!!!!!!!
+ # define available stats
available_stats = [
"line_count",
"function_count",
- "comment_line_count"
+ "comment_line_count",
+ "indentation_level"
]
- # dictionary for the stats UPDATE THIS WHEN NEEDED PLEASE !!!!!!!!
+ # dictionary for the stats
stats_labels = {
"line_count": messages.get("line_count_option", "Line Count"),
"function_count": messages.get("function_count_option", "Function Count"),
- "comment_line_count": messages.get("comment_line_count_option", "Comment Line Count")
+ "comment_line_count": messages.get("comment_line_count_option", "Comment Line Count"),
+ "indentation_level": messages.get("indentation_level_option", "Indentation Analysis")
}
# If --all flag is used, skip the selection menu and use all stats
@@ -62,7 +64,7 @@ def analyze_command(file, all, json_output, LANG_FILE):
try:
# show analyzing message if not in JSON mode
if not json_output:
- print(f"{messages['analyzing_file']}: {file}")
+ print(f"{messages.get('analyzing_file', 'Analyzing file')}: {file}")
# get analysis results from analyze_file
results = analyze_file(file, selected_stats=selected_stat_keys)
@@ -74,8 +76,11 @@ def analyze_command(file, all, json_output, LANG_FILE):
else:
# only print the selected stats in normal mode
for stat in selected_stat_keys:
- if stat in results:
- print(messages[stat].format(count=results[stat]))
+ if stat == "indentation_level" and "indentation_type" in results:
+ print(f"{messages.get('indentation_type', 'Indentation Type')}: {results['indentation_type']}")
+ print(f"{messages.get('indentation_size', 'Indentation Size')}: {results['indentation_size']}")
+ elif stat in results:
+ print(messages.get(stat, f"{stat.replace('_', ' ').title()}: {{count}}").format(count=results[stat]))
except Exception as e:
if json_output:
@@ -84,5 +89,4 @@ def analyze_command(file, all, json_output, LANG_FILE):
error_msg = str(e).replace('\n', ' ')
print(json.dumps({"error": error_msg}))
else:
- print(f"[red]{messages['error']}[/] {e}")
-
+ print(f"{messages.get('error', 'Error')}: {e}")
diff --git a/cli/commands/export/__init__.py b/cli/commands/export/__init__.py
new file mode 100644
index 0000000..efa6dbf
--- /dev/null
+++ b/cli/commands/export/__init__.py
@@ -0,0 +1,3 @@
+"""
+Módulo de inicialização para o pacote de exportação.
+"""
diff --git a/cli/commands/export/export.py b/cli/commands/export/export.py
new file mode 100644
index 0000000..7af48a3
--- /dev/null
+++ b/cli/commands/export/export.py
@@ -0,0 +1,120 @@
+import os
+import json
+import csv
+import typer
+from rich.console import Console
+from rich.table import Table
+
+from cli.utils.get_translation import get_translation
+
+def export_results(results, format_type, output_file, messages):
+ """
+ Export analysis results to a file in the specified format.
+
+ Args:
+ results (dict): Analysis results to export
+ format_type (str): Format to export (json, csv, html, markdown)
+ output_file (str): Path to output file
+ messages (dict): Translation messages
+
+ Returns:
+ bool: True if export was successful, False otherwise
+ """
+ try:
+ # Create directory if it doesn't exist
+ os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
+
+ if format_type == "json":
+ with open(output_file, "w", encoding="utf-8") as f:
+ json.dump(results, f, indent=2)
+
+ elif format_type == "csv":
+ with open(output_file, "w", encoding="utf-8", newline="") as f:
+ writer = csv.writer(f)
+ # Write header
+ writer.writerow(["Metric", "Value"])
+ # Write data
+ for key, value in results.items():
+ if isinstance(value, (int, float, str)):
+ writer.writerow([key, value])
+ elif isinstance(value, list):
+ writer.writerow([key, json.dumps(value)])
+
+ elif format_type == "markdown":
+ with open(output_file, "w", encoding="utf-8") as f:
+ f.write(f"# {messages.get('analysis_results', 'Analysis Results')}\n\n")
+ f.write(f"**{messages.get('file_name', 'File')}: {results.get('file_name', 'Unknown')}**\n\n")
+ f.write("| Metric | Value |\n")
+ f.write("|--------|-------|\n")
+ for key, value in results.items():
+ if isinstance(value, (int, float, str)):
+ f.write(f"| {key.replace('_', ' ').title()} | {value} |\n")
+ elif isinstance(value, list) and key == "indentation_levels":
+ f.write(f"| {key.replace('_', ' ').title()} | {len(value)} levels |\n")
+
+ elif format_type == "html":
+ with open(output_file, "w", encoding="utf-8") as f:
+ f.write("\n\n
\n")
+ f.write("\n")
+ f.write("SpiceCode Analysis Results\n")
+ f.write("\n\n\n")
+ f.write(f"{messages.get('analysis_results', 'Analysis Results')}
\n")
+ f.write(f"{messages.get('file_name', 'File')}: {results.get('file_name', 'Unknown')}
\n")
+ f.write("\n| Metric | Value |
\n")
+ for key, value in results.items():
+ if isinstance(value, (int, float, str)):
+ f.write(f"| {key.replace('_', ' ').title()} | {value} |
\n")
+ elif isinstance(value, list) and key == "indentation_levels":
+ f.write(f"| {key.replace('_', ' ').title()} | {len(value)} levels |
\n")
+ f.write("
\n\n")
+
+ else:
+ return False
+
+ return True
+
+ except Exception as e:
+ print(f"{messages.get('export_error', 'Export error')}: {str(e)}")
+ return False
+
+def export_command(file, format_type, output, LANG_FILE):
+ """
+ Export analysis results to a file.
+ """
+ # Load translations
+ messages = get_translation(LANG_FILE)
+ console = Console()
+
+ # Validate format type
+ valid_formats = ["json", "csv", "markdown", "html"]
+ if format_type not in valid_formats:
+ console.print(f"[red]{messages.get('invalid_format', 'Invalid format')}[/] {format_type}")
+ console.print(f"{messages.get('valid_formats', 'Valid formats')}: {', '.join(valid_formats)}")
+ return
+
+ try:
+ # Analyze file
+ from spice.analyze import analyze_file
+ results = analyze_file(file)
+
+ # Set default output file if not provided
+ if not output:
+ base_name = os.path.splitext(os.path.basename(file))[0]
+ output = f"{base_name}_analysis.{format_type}"
+
+ # Export results
+ success = export_results(results, format_type, output, messages)
+
+ if success:
+ console.print(f"[green]{messages.get('export_success', 'Export successful')}[/]: {output}")
+ else:
+ console.print(f"[red]{messages.get('export_failed', 'Export failed')}[/]")
+
+ except Exception as e:
+ console.print(f"[red]{messages.get('error', 'Error')}[/]: {str(e)}")
diff --git a/cli/main.py b/cli/main.py
index 87c459a..13a9f79 100644
--- a/cli/main.py
+++ b/cli/main.py
@@ -7,7 +7,7 @@
from cli.commands.hello import hello_command
from cli.commands.version import version_command
from cli.commands.analyze import analyze_command
-
+from cli.commands.export.export import export_command
# initialize typer
app = typer.Typer()
@@ -18,48 +18,30 @@
# get current directory, this is needed for it to work on other peoples computers via pip
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
-# select a file to save the current selected langague (if saved to memory it wont persist between commands)
+# select a file to save the current selected language
LANG_FILE = os.path.join(CURRENT_DIR, "lang.txt")
-
-
-# SPICE TRANSLATE COMMAND
@app.command()
def translate():
"""
Set the language for CLI messages.
"""
-
translate_command(LANG_FILE)
-# -- end -- #
-
-
-# SPICE HELLO COMMAND
@app.command()
def hello():
"""
Welcome message.
"""
-
hello_command(LANG_FILE)
-# -- end -- #
-
-
-# SPICE VERSION COMMAND
@app.command()
def version():
"""
Display the current version of the application.
"""
-
version_command(LANG_FILE, CURRENT_DIR)
-#--- end ---#
-
-
-# SPICE ANALYZE COMMAND
@app.command()
def analyze(
file: str,
@@ -69,20 +51,21 @@ def analyze(
"""
Analyze the given file.
"""
-
analyze_command(file, all, json_output, LANG_FILE)
-# -- end -- #
-
+@app.command()
+def export(
+ file: str,
+ format_type: str = typer.Option("json", "--format", "-f", help="Export format (json, csv, markdown, html)"),
+ output: str = typer.Option(None, "--output", "-o", help="Output file path")
+):
+ """
+ Analyze a file and export results to a file in the specified format.
+ """
+ export_command(file, format_type, output, LANG_FILE)
def main():
app() # run typer
-# -- end -- #
-
-
-# whatever the fuck this is python makes no sense
if __name__ == "__main__":
main()
-
-# -- end -- #
\ No newline at end of file
diff --git a/lexers/golang/golexer.py b/lexers/golang/golexer.py
index 2822cc6..db24f4f 100644
--- a/lexers/golang/golexer.py
+++ b/lexers/golang/golexer.py
@@ -186,8 +186,21 @@ def tokenize_string(self):
elif char == "\\": # escape de caracteres
self.position += 1
self.column += 1
+ if self.position < len(self.source_code): # avança o caractere escapado
+ self.position += 1
+ self.column += 1
+ continue
+ elif char == "\n": # se tiver uma nova linha dentro da string
+ # Em Go, strings com aspas duplas não podem conter quebras de linha
+ # mas strings com crases (raw strings) podem
+ if quote_char == '"':
+ return Token(TokenType.ERROR, "string não fechada", self.line, start_col)
+ self.line += 1
+ self.column = 1
+ self.current_line_start = self.position + 1
+ else:
+ self.column += 1
self.position += 1
- self.column += 1
string_value = self.source_code[start_pos:self.position] # pega o texto da string
return Token(TokenType.STRING, string_value, self.line, start_col)
@@ -210,9 +223,9 @@ def tokenize_identifier(self):
def match_operator(self):
"""tenta casar com um operador."""
for op in sorted(self.OPERATORS, key=len, reverse=True): # verifica operadores mais longos primeiro
- if self.source_code.startswith(op, self.position):
+ if self.position + len(op) <= len(self.source_code) and self.source_code[self.position:self.position + len(op)] == op:
token = Token(TokenType.OPERATOR, op, self.line, self.column)
self.position += len(op)
self.column += len(op)
return token
- return None
\ No newline at end of file
+ return None
diff --git a/lexers/javascript/javascriptlexer.py b/lexers/javascript/javascriptlexer.py
index 6a7c33a..7597c9e 100644
--- a/lexers/javascript/javascriptlexer.py
+++ b/lexers/javascript/javascriptlexer.py
@@ -163,4 +163,70 @@ def tokenize_number(self):
self.position += 1
self.column += 1
- return Token(TokenType.NUMBER, self.source_code[start_pos:self.position], self.line, self.column - (self.position - start_pos))
\ No newline at end of file
+ return Token(TokenType.NUMBER, self.source_code[start_pos:self.position], self.line, self.column - (self.position - start_pos))
+
+ def tokenize_identifier(self):
+ """tokeniza um identificador (nomes de variáveis, funções, etc.)."""
+ start_pos = self.position # posição inicial do identificador
+ start_col = self.column # coluna inicial do identificador
+
+ # Usa o padrão de regex para identificadores ou faz a tokenização manual
+ match = self.IDENTIFIER_PATTERN.match(self.source_code[self.position:])
+ if match:
+ identifier = match.group(0) # pega o identificador
+ self.position += len(identifier) # avança a posição
+ self.column += len(identifier) # avança a coluna
+ else:
+ # tokenização manual como fallback
+ while self.position < len(self.source_code) and (self.source_code[self.position].isalnum() or self.source_code[self.position] == "_"):
+ self.position += 1
+ self.column += 1
+ identifier = self.source_code[start_pos:self.position] # pega o texto do identificador
+
+ # verifica se é uma palavra-chave
+ if identifier in self.KEYWORDS:
+ return Token(TokenType.KEYWORD, identifier, self.line, start_col)
+ else:
+ return Token(TokenType.IDENTIFIER, identifier, self.line, start_col)
+
+ def tokenize_string(self):
+ """tokeniza uma string (aspas simples ou duplas)."""
+ start_pos = self.position # posição inicial da string
+ start_col = self.column # coluna inicial da string
+ quote_char = self.source_code[self.position] # tipo de aspas (' ou ")
+ self.position += 1 # avança as aspas iniciais
+ self.column += 1
+
+ while self.position < len(self.source_code):
+ char = self.source_code[self.position]
+ if char == quote_char: # fecha a string
+ self.position += 1
+ self.column += 1
+ break
+ elif char == "\\": # escape de caracteres
+ self.position += 1
+ self.column += 1
+ if self.position < len(self.source_code):
+ self.position += 1
+ self.column += 1
+ continue
+ elif char == "\n": # se tiver uma nova linha dentro da string
+ self.line += 1
+ self.column = 1
+ self.current_line_start = self.position + 1
+ else:
+ self.column += 1
+ self.position += 1
+
+ string_value = self.source_code[start_pos:self.position] # pega o texto da string
+ return Token(TokenType.STRING, string_value, self.line, start_col)
+
+ def match_operator(self):
+ """tenta casar com um operador."""
+ for op in sorted(self.OPERATORS, key=len, reverse=True): # verifica operadores mais longos primeiro
+ if self.source_code.startswith(op, self.position):
+ token = Token(TokenType.OPERATOR, op, self.line, self.column)
+ self.position += len(op)
+ self.column += len(op)
+ return token
+ return None
diff --git a/spice/analyze.py b/spice/analyze.py
index e8b9ef9..3c4eaa6 100644
--- a/spice/analyze.py
+++ b/spice/analyze.py
@@ -1,11 +1,7 @@
import os
-# gustavo testando alguma coisa
from spice.analyzers.identation import detect_indentation
-
-
-# this is the analyze function
def analyze_file(file_path: str, selected_stats=None):
"""
Analyze a file and return only the requested stats.
@@ -19,9 +15,9 @@ def analyze_file(file_path: str, selected_stats=None):
"""
# default to all stats if none specified
if selected_stats is None:
- selected_stats = ["line_count", "function_count", "comment_line_count", "identation_level"]
+ selected_stats = ["line_count", "function_count", "comment_line_count", "indentation_level"]
- # initialize results with the file name (dont change this please)
+ # initialize results with the file name
results = {
"file_name": os.path.basename(file_path)
}
@@ -40,15 +36,15 @@ def analyze_file(file_path: str, selected_stats=None):
from spice.analyzers.count_comment_lines import count_comment_lines
results["comment_line_count"] = count_comment_lines(code)
- # @gtins botei sua funcao aqui pq ela usa o codigo raw e nao o tokenizado, ai so tirei ela ali de baixo pra nao ficar chamando o parser sem precisar
- # edit: ok i see whats going on, instead of appending the results to the resuls, this will itself print the results to the terminal
- # TODO: make analyze_code_structure return the results, then append those results to the results array
- if "identation_level" in selected_stats:
- analyze_code_structure(code)
+ # indentation analysis if requested
+ if "indentation_level" in selected_stats:
+ indentation_info = detect_indentation(code)
+ results["indentation_type"] = indentation_info["indent_type"]
+ results["indentation_size"] = indentation_info["indent_size"]
+ results["indentation_levels"] = indentation_info["levels"]
- # only put the code through the lexer and proceed with tokenization if we need function count (UPDATE THIS WHEN NEEDED PLEASE !!!!!!!!)
- if "function_count" in selected_stats:
-
+ # only put the code through the lexer and proceed with tokenization if needed
+ if any(stat in selected_stats for stat in ["function_count"]):
# get the lexer for the code's language
from spice.utils.get_lexer import get_lexer_for_file
LexerClass = get_lexer_for_file(file_path)
@@ -57,13 +53,12 @@ def analyze_file(file_path: str, selected_stats=None):
lexer = LexerClass(code)
tokens = lexer.tokenize()
- # only put the code through the parser and proceed with parsing if we need function count (UPDATE THIS WHEN NEEDED PLEASE !!!!!!!!)
+ # only put the code through the parser and proceed with parsing if needed
if "function_count" in selected_stats:
-
- # import parser here to avoid error i still dont know why but it works
+ # import parser here to avoid circular import issues
from parser.parser import Parser
- # prase tokens into AST
+ # parse tokens into AST
parser = Parser(tokens)
ast = parser.parse()
@@ -72,24 +67,3 @@ def analyze_file(file_path: str, selected_stats=None):
results["function_count"] = count_functions(ast)
return results
-
-
-
-
-
-# im not sure what to do with this part 😂
-# this is the identation analyzer
-# but it's not included in the menu?
-# im not going to change this since gtins knows better than me how this works
-# but this needs to be refactores and included directly into the analyze_file function and the analyze menu
-def analyze_code_structure(code):
- indentation_info = detect_indentation(code)
-
- print(f"Detected Indentation Type: {indentation_info['indent_type']}")
- print(f"Detected Indentation Size: {indentation_info['indent_size']}")
- for line, level in indentation_info["levels"]:
- # print(f"Indentation Level {level}: {line}")
- print(f"Detected Indentation Type: {indentation_info['indent_type']}")
- print(f"Detected Indentation Size: {indentation_info['indent_size']}")
-
-# ----------------------------------------------------------------------------------------------------
\ No newline at end of file