c118996746
- SwiftUI macOS app with C++17 code analysis engine (ObjC++ bridge) - Agentic AI loop: LLM plans → tool calls → execution → feedback loop - 15 agent tools: file ops, terminal, git, xcode build, code intel - 7 persistent terminal tools with background session management - Chat sidebar with agent step rendering and auto-apply - NVIDIA NIM API integration (Llama 3.3 70B default) - OpenAI tool_calls format with prompt-based fallback - Code editor with syntax highlighting and multi-tab support - File tree, console view, terminal view - Git integration and workspace management
984 lines
39 KiB
C++
984 lines
39 KiB
C++
#include "CodeEngine.hpp"
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
#include <stack>
|
|
#include <cmath>
|
|
#include <set>
|
|
#include <unordered_map>
|
|
#include <cctype>
|
|
|
|
// ─── Swift keyword set ───────────────────────────────────────────
|
|
|
|
static const std::set<std::string>& swift_keyword_set() {
|
|
static const std::set<std::string> kw = {
|
|
"actor", "any", "as", "associatedtype", "async", "await",
|
|
"break", "case", "catch", "class", "continue", "convenience",
|
|
"default", "defer", "deinit", "do", "dynamic",
|
|
"else", "enum", "extension",
|
|
"fallthrough", "false", "fileprivate", "final", "for", "func",
|
|
"get", "guard",
|
|
"if", "import", "in", "indirect", "infix", "init", "inout", "internal", "is",
|
|
"lazy", "let",
|
|
"mutating",
|
|
"nil", "nonisolated",
|
|
"open", "operator", "optional", "override",
|
|
"postfix", "precedencegroup", "prefix", "private", "protocol", "public",
|
|
"repeat", "required", "rethrows", "return",
|
|
"self", "Self", "Sendable", "set", "some", "static", "struct", "subscript", "super", "switch",
|
|
"throw", "throws", "true", "try", "typealias",
|
|
"unowned",
|
|
"var",
|
|
"weak", "where", "while",
|
|
"#available", "#colorLiteral", "#column", "#dsohandle", "#else", "#elseif",
|
|
"#endif", "#error", "#file", "#fileLiteral", "#function", "#if",
|
|
"#imageLiteral", "#line", "#selector", "#sourceLocation", "#warning",
|
|
"@available", "@discardableResult", "@dynamicCallable", "@dynamicMemberLookup",
|
|
"@escaping", "@frozen", "@inlinable", "@main", "@objc", "@objcMembers",
|
|
"@propertyWrapper", "@resultBuilder", "@Sendable", "@testable", "@unchecked", "@unknown"
|
|
};
|
|
return kw;
|
|
}
|
|
|
|
// ─── Construction ────────────────────────────────────────────────
|
|
|
|
CodeEngine::CodeEngine() : m_config() {}
|
|
|
|
CodeEngine::CodeEngine(const EngineConfig& config) : m_config(config) {}
|
|
|
|
CodeEngine::~CodeEngine() {}
|
|
|
|
void CodeEngine::set_config(const EngineConfig& config) {
|
|
m_config = config;
|
|
}
|
|
|
|
EngineConfig CodeEngine::get_config() const {
|
|
return m_config;
|
|
}
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|
|
|
std::string CodeEngine::trim(const std::string& s) const {
|
|
size_t start = s.find_first_not_of(" \t\r\n");
|
|
if (start == std::string::npos) return "";
|
|
size_t end = s.find_last_not_of(" \t\r\n");
|
|
return s.substr(start, end - start + 1);
|
|
}
|
|
|
|
bool CodeEngine::is_swift_keyword(const std::string& word) const {
|
|
return swift_keyword_set().count(word) > 0;
|
|
}
|
|
|
|
std::string CodeEngine::detect_access_level(const std::string& line) const {
|
|
std::string trimmed = trim(line);
|
|
if (trimmed.find("public ") == 0 || trimmed.find("open ") == 0) return "public";
|
|
if (trimmed.find("private ") == 0) return "private";
|
|
if (trimmed.find("fileprivate ") == 0) return "fileprivate";
|
|
if (trimmed.find("internal ") == 0) return "internal";
|
|
return "internal"; // Swift default
|
|
}
|
|
|
|
// ─── Tokenizer ───────────────────────────────────────────────────
|
|
|
|
std::vector<Token> CodeEngine::tokenize(const std::string& code) {
|
|
std::vector<Token> tokens;
|
|
int line = 1, col = 0;
|
|
size_t i = 0;
|
|
|
|
while (i < code.size()) {
|
|
char c = code[i];
|
|
|
|
// Newline
|
|
if (c == '\n') {
|
|
tokens.push_back({TokenType::Newline, "\n", line, col, 1});
|
|
line++;
|
|
col = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Whitespace
|
|
if (c == ' ' || c == '\t' || c == '\r') {
|
|
size_t start = i;
|
|
while (i < code.size() && (code[i] == ' ' || code[i] == '\t' || code[i] == '\r')) i++;
|
|
tokens.push_back({TokenType::Whitespace, code.substr(start, i - start), line, col, static_cast<int>(i - start)});
|
|
col += static_cast<int>(i - start);
|
|
continue;
|
|
}
|
|
|
|
// Line comment
|
|
if (c == '/' && i + 1 < code.size() && code[i + 1] == '/') {
|
|
size_t start = i;
|
|
while (i < code.size() && code[i] != '\n') i++;
|
|
tokens.push_back({TokenType::Comment, code.substr(start, i - start), line, col, static_cast<int>(i - start)});
|
|
col += static_cast<int>(i - start);
|
|
continue;
|
|
}
|
|
|
|
// Block comment
|
|
if (c == '/' && i + 1 < code.size() && code[i + 1] == '*') {
|
|
size_t start = i;
|
|
i += 2;
|
|
int depth = 1; // Support nested block comments (Swift allows them)
|
|
while (i < code.size() && depth > 0) {
|
|
if (code[i] == '/' && i + 1 < code.size() && code[i + 1] == '*') { depth++; i += 2; continue; }
|
|
if (code[i] == '*' && i + 1 < code.size() && code[i + 1] == '/') { depth--; i += 2; continue; }
|
|
if (code[i] == '\n') { line++; col = 0; }
|
|
i++;
|
|
}
|
|
tokens.push_back({TokenType::Comment, code.substr(start, i - start), line, col, static_cast<int>(i - start)});
|
|
continue;
|
|
}
|
|
|
|
// String literal (handles multi-line """ and interpolation depth)
|
|
if (c == '"') {
|
|
size_t start = i;
|
|
bool multiline = (i + 2 < code.size() && code[i + 1] == '"' && code[i + 2] == '"');
|
|
if (multiline) {
|
|
i += 3;
|
|
while (i + 2 < code.size()) {
|
|
if (code[i] == '"' && code[i + 1] == '"' && code[i + 2] == '"') { i += 3; break; }
|
|
if (code[i] == '\n') { line++; col = 0; }
|
|
i++;
|
|
}
|
|
} else {
|
|
i++; // skip opening quote
|
|
while (i < code.size() && code[i] != '"' && code[i] != '\n') {
|
|
if (code[i] == '\\') i++; // skip escaped char
|
|
i++;
|
|
}
|
|
if (i < code.size() && code[i] == '"') i++; // skip closing quote
|
|
}
|
|
tokens.push_back({TokenType::StringLiteral, code.substr(start, i - start), line, col, static_cast<int>(i - start)});
|
|
col += static_cast<int>(i - start);
|
|
continue;
|
|
}
|
|
|
|
// Number literal
|
|
if (std::isdigit(c) || (c == '.' && i + 1 < code.size() && std::isdigit(code[i + 1]))) {
|
|
size_t start = i;
|
|
bool hasDecimal = false;
|
|
if (c == '0' && i + 1 < code.size() && (code[i + 1] == 'x' || code[i + 1] == 'b' || code[i + 1] == 'o')) {
|
|
i += 2; // hex/binary/octal prefix
|
|
while (i < code.size() && (std::isxdigit(code[i]) || code[i] == '_')) i++;
|
|
} else {
|
|
while (i < code.size() && (std::isdigit(code[i]) || code[i] == '_' || code[i] == '.')) {
|
|
if (code[i] == '.') {
|
|
if (hasDecimal) break;
|
|
hasDecimal = true;
|
|
}
|
|
i++;
|
|
}
|
|
// Scientific notation
|
|
if (i < code.size() && (code[i] == 'e' || code[i] == 'E')) {
|
|
i++;
|
|
if (i < code.size() && (code[i] == '+' || code[i] == '-')) i++;
|
|
while (i < code.size() && std::isdigit(code[i])) i++;
|
|
}
|
|
}
|
|
tokens.push_back({TokenType::NumberLiteral, code.substr(start, i - start), line, col, static_cast<int>(i - start)});
|
|
col += static_cast<int>(i - start);
|
|
continue;
|
|
}
|
|
|
|
// Identifier or keyword
|
|
if (std::isalpha(c) || c == '_' || c == '@' || c == '#') {
|
|
size_t start = i;
|
|
i++;
|
|
while (i < code.size() && (std::isalnum(code[i]) || code[i] == '_')) i++;
|
|
std::string word = code.substr(start, i - start);
|
|
TokenType type = is_swift_keyword(word) ? TokenType::Keyword : TokenType::Identifier;
|
|
tokens.push_back({type, word, line, col, static_cast<int>(i - start)});
|
|
col += static_cast<int>(i - start);
|
|
continue;
|
|
}
|
|
|
|
// Operators and punctuation
|
|
const std::string operators = "+-*/%=<>!&|^~?";
|
|
const std::string punctuation = "{}()[],:;.@#";
|
|
|
|
if (punctuation.find(c) != std::string::npos) {
|
|
tokens.push_back({TokenType::Punctuation, std::string(1, c), line, col, 1});
|
|
col++;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (operators.find(c) != std::string::npos) {
|
|
// Consume multi-char operators
|
|
size_t start = i;
|
|
i++;
|
|
while (i < code.size() && operators.find(code[i]) != std::string::npos) i++;
|
|
tokens.push_back({TokenType::Operator, code.substr(start, i - start), line, col, static_cast<int>(i - start)});
|
|
col += static_cast<int>(i - start);
|
|
continue;
|
|
}
|
|
|
|
// Unknown
|
|
tokens.push_back({TokenType::Unknown, std::string(1, c), line, col, 1});
|
|
col++;
|
|
i++;
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
// ─── Symbol Extraction ───────────────────────────────────────────
|
|
|
|
std::vector<SymbolInfo> CodeEngine::extract_symbols(const std::string& code) {
|
|
std::vector<SymbolInfo> symbols;
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
int lineNum = 0;
|
|
|
|
while (std::getline(stream, line)) {
|
|
lineNum++;
|
|
std::string trimmed = trim(line);
|
|
if (trimmed.empty() || trimmed[0] == '/' || trimmed[0] == '*') continue;
|
|
|
|
// Strip access modifiers for parsing
|
|
std::string access = detect_access_level(trimmed);
|
|
std::string body = trimmed;
|
|
for (const auto& prefix : {"public ", "open ", "private ", "fileprivate ", "internal ", "static ", "final ", "override "}) {
|
|
auto pos = body.find(prefix);
|
|
if (pos == 0) body = body.substr(std::string(prefix).size());
|
|
}
|
|
|
|
auto extract_name = [&](const std::string& keyword) -> std::string {
|
|
auto pos = body.find(keyword);
|
|
if (pos != 0) return "";
|
|
std::string rest = body.substr(keyword.size());
|
|
// Trim leading whitespace
|
|
size_t nameStart = rest.find_first_not_of(" \t");
|
|
if (nameStart == std::string::npos) return "";
|
|
size_t nameEnd = rest.find_first_of(" \t({:<", nameStart);
|
|
if (nameEnd == std::string::npos) nameEnd = rest.size();
|
|
return rest.substr(nameStart, nameEnd - nameStart);
|
|
};
|
|
|
|
if (body.find("func ") == 0) {
|
|
std::string name = extract_name("func ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Function, name, lineNum, access, trimmed});
|
|
} else if (body.find("class ") == 0) {
|
|
// Skip "class func" and "class var"
|
|
std::string after = body.substr(6);
|
|
std::string afterTrim = trim(after);
|
|
if (afterTrim.find("func") != 0 && afterTrim.find("var") != 0 && afterTrim.find("let") != 0) {
|
|
std::string name = extract_name("class ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Class, name, lineNum, access, trimmed});
|
|
}
|
|
} else if (body.find("struct ") == 0) {
|
|
std::string name = extract_name("struct ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Struct, name, lineNum, access, trimmed});
|
|
} else if (body.find("enum ") == 0) {
|
|
std::string name = extract_name("enum ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Enum, name, lineNum, access, trimmed});
|
|
} else if (body.find("protocol ") == 0) {
|
|
std::string name = extract_name("protocol ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Protocol, name, lineNum, access, trimmed});
|
|
} else if (body.find("typealias ") == 0) {
|
|
std::string name = extract_name("typealias ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::TypeAlias, name, lineNum, access, trimmed});
|
|
} else if (body.find("extension ") == 0) {
|
|
std::string name = extract_name("extension ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Extension, name, lineNum, access, trimmed});
|
|
} else if (body.find("var ") == 0) {
|
|
std::string name = extract_name("var ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Variable, name, lineNum, access, trimmed});
|
|
} else if (body.find("let ") == 0) {
|
|
std::string name = extract_name("let ");
|
|
if (!name.empty())
|
|
symbols.push_back({SymbolKind::Constant, name, lineNum, access, trimmed});
|
|
}
|
|
}
|
|
|
|
return symbols;
|
|
}
|
|
|
|
// ─── Import Tracking ─────────────────────────────────────────────
|
|
|
|
std::vector<std::string> CodeEngine::extract_imports(const std::string& code) {
|
|
std::vector<std::string> imports;
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
|
|
while (std::getline(stream, line)) {
|
|
std::string trimmed = trim(line);
|
|
if (trimmed.find("import ") == 0) {
|
|
std::string module = trim(trimmed.substr(7));
|
|
// Handle "@testable import X" or "import class X.Y"
|
|
if (module.find("class ") == 0 || module.find("struct ") == 0 ||
|
|
module.find("enum ") == 0 || module.find("func ") == 0 ||
|
|
module.find("var ") == 0 || module.find("let ") == 0 ||
|
|
module.find("protocol ") == 0 || module.find("typealias ") == 0) {
|
|
// Selective import: skip the keyword
|
|
auto spacePos = module.find(' ');
|
|
if (spacePos != std::string::npos) module = trim(module.substr(spacePos + 1));
|
|
}
|
|
if (!module.empty()) imports.push_back(module);
|
|
} else if (trimmed.find("@testable import ") == 0) {
|
|
std::string module = trim(trimmed.substr(17));
|
|
if (!module.empty()) imports.push_back("@testable " + module);
|
|
}
|
|
}
|
|
|
|
return imports;
|
|
}
|
|
|
|
// ─── Duplicate Line Detection ────────────────────────────────────
|
|
|
|
std::vector<std::pair<int, int>> CodeEngine::detect_duplicate_lines(const std::string& code) {
|
|
std::vector<std::pair<int, int>> dupes;
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
std::unordered_map<std::string, int> seen; // content -> first line number
|
|
int lineNum = 0;
|
|
|
|
while (std::getline(stream, line)) {
|
|
lineNum++;
|
|
std::string trimmed = trim(line);
|
|
// Skip blank, short, trivial lines
|
|
if (trimmed.size() < 10 || trimmed == "{" || trimmed == "}" ||
|
|
trimmed == "}" || trimmed == "return" || trimmed[0] == '/' ||
|
|
trimmed == "break" || trimmed == "default:" || trimmed == "else {") {
|
|
continue;
|
|
}
|
|
auto it = seen.find(trimmed);
|
|
if (it != seen.end()) {
|
|
dupes.push_back({it->second, lineNum});
|
|
} else {
|
|
seen[trimmed] = lineNum;
|
|
}
|
|
}
|
|
|
|
return dupes;
|
|
}
|
|
|
|
// ─── TODO/FIXME Extraction ───────────────────────────────────────
|
|
|
|
std::vector<std::pair<int, std::string>> CodeEngine::extract_todo_comments(const std::string& code) {
|
|
std::vector<std::pair<int, std::string>> todos;
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
int lineNum = 0;
|
|
|
|
while (std::getline(stream, line)) {
|
|
lineNum++;
|
|
// Look for TODO:, FIXME:, HACK:, XXX:, MARK: in comments
|
|
auto commentPos = line.find("//");
|
|
if (commentPos == std::string::npos) continue;
|
|
std::string comment = line.substr(commentPos + 2);
|
|
std::string upper;
|
|
upper.reserve(comment.size());
|
|
for (char ch : comment) upper += static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
|
|
|
for (const auto& tag : {"TODO", "FIXME", "HACK", "XXX", "WARNING"}) {
|
|
auto pos = upper.find(tag);
|
|
if (pos != std::string::npos) {
|
|
todos.push_back({lineNum, trim(comment)});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return todos;
|
|
}
|
|
|
|
// ─── Indentation ─────────────────────────────────────────────────
|
|
|
|
int CodeEngine::suggest_indentation(const std::string& codeBefore, int tabWidth) {
|
|
// Count net brace depth from the code preceding the cursor
|
|
int depth = 0;
|
|
bool inString = false;
|
|
bool inLineComment = false;
|
|
bool inBlockComment = false;
|
|
|
|
for (size_t i = 0; i < codeBefore.size(); ++i) {
|
|
char c = codeBefore[i];
|
|
|
|
if (inBlockComment) {
|
|
if (c == '*' && i + 1 < codeBefore.size() && codeBefore[i + 1] == '/') { inBlockComment = false; ++i; }
|
|
continue;
|
|
}
|
|
if (inLineComment) {
|
|
if (c == '\n') inLineComment = false;
|
|
continue;
|
|
}
|
|
if (c == '/' && i + 1 < codeBefore.size()) {
|
|
if (codeBefore[i + 1] == '/') { inLineComment = true; continue; }
|
|
if (codeBefore[i + 1] == '*') { inBlockComment = true; ++i; continue; }
|
|
}
|
|
if (c == '"' && (i == 0 || codeBefore[i - 1] != '\\')) {
|
|
inString = !inString;
|
|
continue;
|
|
}
|
|
if (inString) continue;
|
|
|
|
if (c == '{') depth++;
|
|
else if (c == '}') depth = std::max(0, depth - 1);
|
|
}
|
|
|
|
return depth * tabWidth;
|
|
}
|
|
|
|
// ─── Cyclomatic Complexity ───────────────────────────────────────
|
|
|
|
int CodeEngine::cyclomatic_complexity(const std::string& code) {
|
|
// M = E - N + 2P simplified to: 1 + decision points
|
|
int cc = 1;
|
|
const std::vector<std::string> decisionPatterns = {
|
|
"if ", "else if ", "guard ", "for ", "while ", "repeat ",
|
|
"case ", "catch ", "where ", "&&", "||", "??"
|
|
};
|
|
|
|
for (const auto& pat : decisionPatterns) {
|
|
std::string::size_type pos = 0;
|
|
while ((pos = code.find(pat, pos)) != std::string::npos) {
|
|
cc++;
|
|
pos += pat.size();
|
|
}
|
|
}
|
|
return cc;
|
|
}
|
|
|
|
// ─── Maintainability Index ───────────────────────────────────────
|
|
|
|
double CodeEngine::maintainability_index(const std::string& code) {
|
|
// Simplified Microsoft-style MI formula:
|
|
// MI = max(0, (171 - 5.2*ln(HV) - 0.23*CC - 16.2*ln(LOC)) * 100/171)
|
|
// Using character count as Halstead Volume proxy
|
|
|
|
int loc = count_lines(code);
|
|
int cc = cyclomatic_complexity(code);
|
|
int volume = static_cast<int>(code.size());
|
|
|
|
if (loc == 0 || volume == 0) return 100.0;
|
|
|
|
double lnVol = std::log(static_cast<double>(volume));
|
|
double lnLoc = std::log(static_cast<double>(loc));
|
|
|
|
double mi = 171.0 - 5.2 * lnVol - 0.23 * cc - 16.2 * lnLoc;
|
|
mi = mi * 100.0 / 171.0;
|
|
|
|
return std::max(0.0, std::min(100.0, mi));
|
|
}
|
|
|
|
// ─── Full Analysis ───────────────────────────────────────────────
|
|
|
|
AnalysisReport CodeEngine::full_analysis(const std::string& code) {
|
|
AnalysisReport report;
|
|
report.lineCount = count_lines(code);
|
|
report.charCount = static_cast<int>(code.size());
|
|
report.blankLineCount = count_blank_lines(code);
|
|
report.commentLineCount = 0; // computed via comment_ratio
|
|
report.functionCount = count_functions(code);
|
|
report.classCount = count_classes(code);
|
|
report.structCount = count_structs(code);
|
|
report.enumCount = count_enums(code);
|
|
report.protocolCount = count_protocols(code);
|
|
report.complexity = estimate_complexity(code);
|
|
report.cyclomaticComplexity = cyclomatic_complexity(code);
|
|
report.maintainabilityIndex = maintainability_index(code);
|
|
report.bracesBalanced = check_balanced_braces(code);
|
|
report.keywords = extract_keywords(code);
|
|
report.issues = find_issues(code);
|
|
report.commentRatio = comment_ratio(code);
|
|
report.codeLineCount = report.lineCount - report.blankLineCount;
|
|
report.commentLineCount = static_cast<int>(report.commentRatio * report.lineCount);
|
|
report.symbols = extract_symbols(code);
|
|
report.imports = extract_imports(code);
|
|
report.duplicateLinePairs = detect_duplicate_lines(code);
|
|
report.todoComments = extract_todo_comments(code);
|
|
|
|
// Build keyword frequency map
|
|
const std::vector<std::string> freqKeywords = {
|
|
"import", "class", "struct", "enum", "protocol", "func",
|
|
"var", "let", "if", "else", "for", "while", "switch",
|
|
"return", "guard", "async", "await", "throws", "try",
|
|
"catch", "defer", "extension", "private", "public", "static",
|
|
"override", "final", "weak", "lazy", "mutating"
|
|
};
|
|
for (const auto& kw : freqKeywords) {
|
|
int count = count_pattern(code, kw + " ");
|
|
if (count > 0) {
|
|
report.keywordFrequency[kw] = count;
|
|
}
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
std::string CodeEngine::format_report(const AnalysisReport& report) {
|
|
std::ostringstream out;
|
|
out << "═══════════════════════════════════════════\n";
|
|
out << " CxIDE Analysis Report \n";
|
|
out << "═══════════════════════════════════════════\n\n";
|
|
|
|
// ── Metrics
|
|
out << "📊 Metrics\n";
|
|
out << " Total Lines: " << report.lineCount << "\n";
|
|
out << " Code Lines: " << report.codeLineCount << "\n";
|
|
out << " Blank Lines: " << report.blankLineCount << "\n";
|
|
out << " Comment Lines: " << report.commentLineCount << "\n";
|
|
out << " Characters: " << report.charCount << "\n";
|
|
out << " Comment Ratio: " << static_cast<int>(report.commentRatio * 100) << "%\n\n";
|
|
|
|
// ── Declarations
|
|
out << "📦 Declarations\n";
|
|
out << " Functions: " << report.functionCount << "\n";
|
|
out << " Classes: " << report.classCount << "\n";
|
|
out << " Structs: " << report.structCount << "\n";
|
|
out << " Enums: " << report.enumCount << "\n";
|
|
out << " Protocols: " << report.protocolCount << "\n\n";
|
|
|
|
// ── Complexity
|
|
out << "🧮 Complexity\n";
|
|
out << " Estimate: " << report.complexity << "\n";
|
|
out << " Cyclomatic: " << report.cyclomaticComplexity << "\n";
|
|
out << " Maintainability: " << static_cast<int>(report.maintainabilityIndex) << "/100";
|
|
if (report.maintainabilityIndex >= 80) out << " (Excellent)";
|
|
else if (report.maintainabilityIndex >= 60) out << " (Good)";
|
|
else if (report.maintainabilityIndex >= 40) out << " (Moderate)";
|
|
else out << " (Needs Improvement)";
|
|
out << "\n";
|
|
out << " Braces: " << (report.bracesBalanced ? "✓ Balanced" : "⚠ Unbalanced") << "\n\n";
|
|
|
|
// ── Imports
|
|
if (!report.imports.empty()) {
|
|
out << "📥 Imports (" << report.imports.size() << ")\n";
|
|
for (const auto& imp : report.imports) {
|
|
out << " • " << imp << "\n";
|
|
}
|
|
out << "\n";
|
|
}
|
|
|
|
// ── Symbols
|
|
if (!report.symbols.empty()) {
|
|
out << "🔤 Symbols (" << report.symbols.size() << ")\n";
|
|
for (const auto& sym : report.symbols) {
|
|
const char* kindLabel = "";
|
|
switch (sym.kind) {
|
|
case SymbolKind::Function: kindLabel = "func"; break;
|
|
case SymbolKind::Class: kindLabel = "class"; break;
|
|
case SymbolKind::Struct: kindLabel = "struct"; break;
|
|
case SymbolKind::Enum: kindLabel = "enum"; break;
|
|
case SymbolKind::Protocol: kindLabel = "protocol"; break;
|
|
case SymbolKind::Variable: kindLabel = "var"; break;
|
|
case SymbolKind::Constant: kindLabel = "let"; break;
|
|
case SymbolKind::TypeAlias: kindLabel = "typealias"; break;
|
|
case SymbolKind::Extension: kindLabel = "extension"; break;
|
|
}
|
|
out << " L" << sym.line << " [" << kindLabel << "] "
|
|
<< sym.accessLevel << " " << sym.name << "\n";
|
|
}
|
|
out << "\n";
|
|
}
|
|
|
|
// ── Keywords
|
|
if (!report.keywords.empty()) {
|
|
out << "🔑 Keywords Found (" << report.keywords.size() << ")\n ";
|
|
for (size_t i = 0; i < report.keywords.size(); ++i) {
|
|
if (i > 0) out << ", ";
|
|
out << report.keywords[i];
|
|
}
|
|
out << "\n\n";
|
|
}
|
|
|
|
// ── Keyword Frequency
|
|
if (!report.keywordFrequency.empty()) {
|
|
out << "📈 Keyword Frequency\n";
|
|
for (const auto& pair : report.keywordFrequency) {
|
|
out << " " << pair.first << ": " << pair.second << "\n";
|
|
}
|
|
out << "\n";
|
|
}
|
|
|
|
// ── TODO/FIXME
|
|
if (!report.todoComments.empty()) {
|
|
out << "📝 TODO/FIXME (" << report.todoComments.size() << ")\n";
|
|
for (const auto& todo : report.todoComments) {
|
|
out << " L" << todo.first << ": " << todo.second << "\n";
|
|
}
|
|
out << "\n";
|
|
}
|
|
|
|
// ── Duplicates
|
|
if (!report.duplicateLinePairs.empty()) {
|
|
out << "🔁 Duplicate Lines (" << report.duplicateLinePairs.size() << " pairs)\n";
|
|
for (const auto& pair : report.duplicateLinePairs) {
|
|
out << " L" << pair.first << " ↔ L" << pair.second << "\n";
|
|
}
|
|
out << "\n";
|
|
}
|
|
|
|
// ── Issues
|
|
if (!report.issues.empty()) {
|
|
out << "⚠ Issues (" << report.issues.size() << ")\n";
|
|
for (const auto& issue : report.issues) {
|
|
out << " • " << issue << "\n";
|
|
}
|
|
} else {
|
|
out << "✓ No issues detected.\n";
|
|
}
|
|
|
|
out << "\n═══════════════════════════════════════════\n";
|
|
return out.str();
|
|
}
|
|
|
|
std::string CodeEngine::analyze_syntax(const std::string& code) {
|
|
if (code.empty()) return "Empty file — nothing to analyze.";
|
|
auto report = full_analysis(code);
|
|
return format_report(report);
|
|
}
|
|
|
|
// ─── Checksum ────────────────────────────────────────────────────
|
|
|
|
int CodeEngine::calculate_checksum(const std::string& code) {
|
|
unsigned long hash = 5381;
|
|
for (char c : code) {
|
|
hash = ((hash << 5) + hash) + static_cast<unsigned char>(c);
|
|
}
|
|
return static_cast<int>(hash & 0x7FFFFFFF);
|
|
}
|
|
|
|
// ─── Counting ────────────────────────────────────────────────────
|
|
|
|
int CodeEngine::count_lines(const std::string& code) {
|
|
if (code.empty()) return 0;
|
|
return static_cast<int>(std::count(code.begin(), code.end(), '\n')) + 1;
|
|
}
|
|
|
|
int CodeEngine::count_blank_lines(const std::string& code) {
|
|
int count = 0;
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
while (std::getline(stream, line)) {
|
|
if (trim(line).empty()) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int CodeEngine::count_code_lines(const std::string& code) {
|
|
return count_lines(code) - count_blank_lines(code);
|
|
}
|
|
|
|
int CodeEngine::count_functions(const std::string& code) {
|
|
return count_pattern(code, "func ");
|
|
}
|
|
|
|
int CodeEngine::count_classes(const std::string& code) {
|
|
// Avoid counting "class func" or "class var"
|
|
int total = count_pattern(code, "class ");
|
|
total -= count_pattern(code, "class func ");
|
|
total -= count_pattern(code, "class var ");
|
|
total -= count_pattern(code, "class let ");
|
|
return std::max(0, total);
|
|
}
|
|
|
|
int CodeEngine::count_structs(const std::string& code) {
|
|
return count_pattern(code, "struct ");
|
|
}
|
|
|
|
int CodeEngine::count_enums(const std::string& code) {
|
|
return count_pattern(code, "enum ");
|
|
}
|
|
|
|
int CodeEngine::count_protocols(const std::string& code) {
|
|
return count_pattern(code, "protocol ");
|
|
}
|
|
|
|
int CodeEngine::count_pattern(const std::string& code, const std::string& pattern) {
|
|
int count = 0;
|
|
std::string::size_type pos = 0;
|
|
while ((pos = code.find(pattern, pos)) != std::string::npos) {
|
|
count++;
|
|
pos += pattern.size();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// ─── Complexity ──────────────────────────────────────────────────
|
|
|
|
std::string CodeEngine::estimate_complexity(const std::string& code) {
|
|
int score = 0;
|
|
|
|
const std::vector<std::string> patterns = {
|
|
"if ", "else ", "for ", "while ", "switch ", "guard ", "catch ",
|
|
"case ", "where ", "repeat "
|
|
};
|
|
for (const auto& pat : patterns) {
|
|
std::string::size_type pos = 0;
|
|
while ((pos = code.find(pat, pos)) != std::string::npos) {
|
|
score++;
|
|
pos += pat.size();
|
|
}
|
|
}
|
|
|
|
// Nesting depth adds complexity
|
|
int maxDepth = 0, depth = 0;
|
|
for (char c : code) {
|
|
if (c == '{') { depth++; maxDepth = std::max(maxDepth, depth); }
|
|
if (c == '}') { depth = std::max(0, depth - 1); }
|
|
}
|
|
score += maxDepth * 2;
|
|
|
|
// Function count adds complexity
|
|
score += count_functions(code);
|
|
|
|
if (score <= 4) return "Low (" + std::to_string(score) + ")";
|
|
if (score <= 10) return "Moderate (" + std::to_string(score) + ")";
|
|
if (score <= 20) return "High (" + std::to_string(score) + ")";
|
|
return "Very High (" + std::to_string(score) + ")";
|
|
}
|
|
|
|
// ─── Brace Balancing ─────────────────────────────────────────────
|
|
|
|
bool CodeEngine::check_balanced_braces(const std::string& code) {
|
|
std::stack<char> stk;
|
|
bool inString = false;
|
|
bool inLineComment = false;
|
|
bool inBlockComment = false;
|
|
|
|
for (size_t i = 0; i < code.size(); ++i) {
|
|
char c = code[i];
|
|
|
|
if (inBlockComment) {
|
|
if (c == '*' && i + 1 < code.size() && code[i + 1] == '/') {
|
|
inBlockComment = false;
|
|
++i;
|
|
}
|
|
continue;
|
|
}
|
|
if (inLineComment) {
|
|
if (c == '\n') inLineComment = false;
|
|
continue;
|
|
}
|
|
if (c == '/' && i + 1 < code.size()) {
|
|
if (code[i + 1] == '/') { inLineComment = true; continue; }
|
|
if (code[i + 1] == '*') { inBlockComment = true; ++i; continue; }
|
|
}
|
|
if (c == '"' && (i == 0 || code[i - 1] != '\\')) {
|
|
inString = !inString;
|
|
continue;
|
|
}
|
|
if (inString) continue;
|
|
|
|
if (c == '{' || c == '(' || c == '[') {
|
|
stk.push(c);
|
|
} else if (c == '}' || c == ')' || c == ']') {
|
|
if (stk.empty()) return false;
|
|
char open = stk.top();
|
|
stk.pop();
|
|
if ((c == '}' && open != '{') ||
|
|
(c == ')' && open != '(') ||
|
|
(c == ']' && open != '[')) return false;
|
|
}
|
|
}
|
|
return stk.empty();
|
|
}
|
|
|
|
// ─── Comment Ratio ───────────────────────────────────────────────
|
|
|
|
double CodeEngine::comment_ratio(const std::string& code) {
|
|
if (code.empty()) return 0.0;
|
|
|
|
int commentLines = 0;
|
|
int totalLines = 0;
|
|
bool inBlockComment = false;
|
|
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
while (std::getline(stream, line)) {
|
|
totalLines++;
|
|
std::string trimmed = trim(line);
|
|
if (trimmed.empty()) continue;
|
|
|
|
if (inBlockComment) {
|
|
commentLines++;
|
|
if (trimmed.find("*/") != std::string::npos) {
|
|
inBlockComment = false;
|
|
}
|
|
continue;
|
|
}
|
|
if (trimmed.substr(0, 2) == "//") {
|
|
commentLines++;
|
|
}
|
|
if (trimmed.substr(0, 2) == "/*") {
|
|
commentLines++;
|
|
if (trimmed.find("*/") == std::string::npos) {
|
|
inBlockComment = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return totalLines > 0 ? static_cast<double>(commentLines) / totalLines : 0.0;
|
|
}
|
|
|
|
// ─── Keyword Extraction ─────────────────────────────────────────
|
|
|
|
std::vector<std::string> CodeEngine::extract_keywords(const std::string& code) {
|
|
const std::vector<std::string> swiftKeywords = {
|
|
"import", "class", "struct", "enum", "protocol", "func",
|
|
"var", "let", "if", "else", "for", "while", "switch",
|
|
"return", "guard", "async", "await", "throws", "try",
|
|
"catch", "defer", "extension", "private", "public", "static",
|
|
"override", "final", "weak", "lazy", "mutating", "typealias",
|
|
"init", "deinit", "subscript", "associatedtype", "where",
|
|
"inout", "some", "any", "actor", "nonisolated", "Sendable"
|
|
};
|
|
|
|
std::vector<std::string> found;
|
|
for (const auto& kw : swiftKeywords) {
|
|
std::string searchSpace = kw + " ";
|
|
std::string searchNewline = kw + "\n";
|
|
std::string searchParen = kw + "(";
|
|
std::string searchBrace = kw + "{";
|
|
std::string searchColon = kw + ":";
|
|
if (code.find(searchSpace) != std::string::npos ||
|
|
code.find(searchNewline) != std::string::npos ||
|
|
code.find(searchParen) != std::string::npos ||
|
|
code.find(searchBrace) != std::string::npos ||
|
|
code.find(searchColon) != std::string::npos) {
|
|
found.push_back(kw);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// ─── Issue Detection ─────────────────────────────────────────────
|
|
|
|
std::vector<std::string> CodeEngine::find_issues(const std::string& code) {
|
|
std::vector<std::string> issues;
|
|
|
|
// Force unwraps
|
|
if (m_config.detectForceUnwraps) {
|
|
int forceUnwraps = 0;
|
|
for (size_t i = 1; i < code.size(); ++i) {
|
|
if (code[i] == '!' && code[i - 1] != '=' && code[i - 1] != '<' &&
|
|
code[i - 1] != '>' && code[i - 1] != ' ' && code[i - 1] != '"') {
|
|
if (i + 1 >= code.size() || code[i + 1] != '=') {
|
|
forceUnwraps++;
|
|
}
|
|
}
|
|
}
|
|
if (forceUnwraps > 0) {
|
|
issues.push_back("Found " + std::to_string(forceUnwraps) +
|
|
" potential force unwrap(s). Consider guard/if-let.");
|
|
}
|
|
}
|
|
|
|
if (m_config.detectForceTry && code.find("try!") != std::string::npos) {
|
|
issues.push_back("Usage of try! detected. Consider try/catch for safety.");
|
|
}
|
|
if (m_config.detectForceCast && code.find("as!") != std::string::npos) {
|
|
issues.push_back("Force cast (as!) detected. Consider using as? instead.");
|
|
}
|
|
|
|
// Print statement count
|
|
int prints = count_pattern(code, "print(");
|
|
if (prints > m_config.maxPrintStatements) {
|
|
issues.push_back("Heavy use of print() (" + std::to_string(prints) +
|
|
" calls). Consider a logging framework.");
|
|
}
|
|
|
|
// Long lines
|
|
if (m_config.detectLongLines) {
|
|
int longLines = 0;
|
|
std::istringstream stream(code);
|
|
std::string line;
|
|
while (std::getline(stream, line)) {
|
|
if (static_cast<int>(line.size()) > m_config.maxLineLength) longLines++;
|
|
}
|
|
if (longLines > 0) {
|
|
issues.push_back(std::to_string(longLines) + " line(s) exceed " +
|
|
std::to_string(m_config.maxLineLength) + " characters.");
|
|
}
|
|
}
|
|
|
|
// Deeply nested code
|
|
if (m_config.detectDeepNesting) {
|
|
int maxDepth = 0, depth = 0;
|
|
for (char c : code) {
|
|
if (c == '{') { depth++; maxDepth = std::max(maxDepth, depth); }
|
|
if (c == '}') { depth = std::max(0, depth - 1); }
|
|
}
|
|
if (maxDepth > m_config.maxNestingDepth) {
|
|
issues.push_back("Deep nesting detected (depth: " + std::to_string(maxDepth) +
|
|
", max: " + std::to_string(m_config.maxNestingDepth) +
|
|
"). Consider refactoring.");
|
|
}
|
|
}
|
|
|
|
// Low comment ratio
|
|
if (m_config.detectLowComments) {
|
|
double ratio = comment_ratio(code);
|
|
if (count_lines(code) > m_config.minLinesForCommentCheck && ratio < m_config.minCommentRatio) {
|
|
issues.push_back("Low comment ratio (" + std::to_string(static_cast<int>(ratio * 100)) +
|
|
"%). Consider adding documentation.");
|
|
}
|
|
}
|
|
|
|
// Large functions
|
|
if (m_config.detectLargeFunctions) {
|
|
int funcCount = count_functions(code);
|
|
int lineCount = count_lines(code);
|
|
if (funcCount > 0 && lineCount / funcCount > m_config.maxFunctionLength) {
|
|
issues.push_back("Average function length is high (~" +
|
|
std::to_string(lineCount / funcCount) +
|
|
" lines, max: " + std::to_string(m_config.maxFunctionLength) +
|
|
"). Consider splitting into smaller functions.");
|
|
}
|
|
}
|
|
|
|
// Duplicate lines
|
|
if (m_config.detectDuplicateLines) {
|
|
auto dupes = detect_duplicate_lines(code);
|
|
if (dupes.size() > 3) {
|
|
issues.push_back(std::to_string(dupes.size()) +
|
|
" duplicate line pairs detected. Consider extracting shared logic.");
|
|
}
|
|
}
|
|
|
|
// TODO/FIXME reminders
|
|
if (m_config.detectTodoFixme) {
|
|
auto todos = extract_todo_comments(code);
|
|
if (!todos.empty()) {
|
|
issues.push_back(std::to_string(todos.size()) + " TODO/FIXME comment(s) found.");
|
|
}
|
|
}
|
|
|
|
// Retain cycle risk: closures capturing self without [weak self]
|
|
if (m_config.detectRetainCycles) {
|
|
int closuresWithSelf = 0;
|
|
std::string::size_type pos = 0;
|
|
while ((pos = code.find("{ [", pos)) != std::string::npos) {
|
|
pos += 3;
|
|
}
|
|
// Heuristic: look for ".self" or "self." inside closures without [weak self]
|
|
pos = 0;
|
|
while ((pos = code.find("self.", pos)) != std::string::npos) {
|
|
closuresWithSelf++;
|
|
pos += 5;
|
|
}
|
|
if (closuresWithSelf > 10) {
|
|
issues.push_back("High self. usage (" + std::to_string(closuresWithSelf) +
|
|
" refs). Verify no retain cycles in closures.");
|
|
}
|
|
}
|
|
|
|
// Maintainability warning
|
|
double mi = maintainability_index(code);
|
|
if (mi < 40 && count_lines(code) > 30) {
|
|
issues.push_back("Low maintainability index (" + std::to_string(static_cast<int>(mi)) +
|
|
"/100). Consider refactoring for readability.");
|
|
}
|
|
|
|
return issues;
|
|
} |