Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"gpt-tokenizer": "^2.1.2",
"groq-sdk": "^0.8.0",
"octokit": "^3.1.1",
"tree-sitter-python": "^0.23.4",
"web-tree-sitter": "^0.24.4",
"xml2js": "^0.6.2"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { Review } from "./constants";
import { env } from "./env";
import { processPullRequest } from "./review-agent";
import { applyReview } from "./reviews";
import * as fs from 'fs';

// This creates a new instance of the Octokit App class.
const reviewApp = new App({
appId: env.GITHUB_APP_ID,
privateKey: env.GITHUB_PRIVATE_KEY,
privateKey: fs.readFileSync(env.GITHUB_PRIVATE_KEY, 'utf-8'),
webhooks: {
secret: env.GITHUB_WEBHOOK_SECRET,
},
Expand Down
25 changes: 19 additions & 6 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Node } from "@babel/traverse";
import { JavascriptParser } from "./context/language/javascript-parser";
import { ChatCompletionMessageParam } from "groq-sdk/resources/chat/completions";
import { PythonParser } from "./context/language/python-parser";
import { SyntaxNode } from "web-tree-sitter";

export interface PRFile {
sha: string;
Expand Down Expand Up @@ -90,28 +92,39 @@ export const processGitFilepath = (filepath: string) => {
};

export interface EnclosingContext {
enclosingContext: Node | null;
enclosingContext: Node | SyntaxNode | null;
}

export interface AbstractParser {
findEnclosingContext(
file: string,
lineStart: number,
lineEnd: number
): EnclosingContext;
dryRun(file: string): { valid: boolean; error: string };
): Promise<EnclosingContext>;

dryRun(
file: string
): Promise<{ valid: boolean; error: string }>;
}

const EXTENSIONS_TO_PARSERS: Map<string, AbstractParser> = new Map([
["ts", new JavascriptParser()],
["tsx", new JavascriptParser()],
["js", new JavascriptParser()],
["jsx", new JavascriptParser()],
["py", new PythonParser()],
]);

export const getParserForExtension = (filename: string) => {
const fileExtension = filename.split(".").pop().toLowerCase();
return EXTENSIONS_TO_PARSERS.get(fileExtension) || null;
export const getParserForExtension = (filename: string): AbstractParser => {
console.log(`πŸ”§ Getting parser for file: ${filename}`);
const extension = filename.split('.').pop()?.toLowerCase();
console.log(`Extension detected: ${extension}`);

// Add logging for what parser is being returned
const parser = EXTENSIONS_TO_PARSERS.get(extension);
console.log(`Parser found: ${parser ? parser.constructor.name : 'None'}`);

return parser;
};

export const assignLineNumbers = (contents: string): string => {
Expand Down
6 changes: 3 additions & 3 deletions src/context/language/javascript-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ const processNode = (
};

export class JavascriptParser implements AbstractParser {
findEnclosingContext(
async findEnclosingContext(
file: string,
lineStart: number,
lineEnd: number
): EnclosingContext {
): Promise<EnclosingContext> {
const ast = parser.parse(file, {
sourceType: "module",
plugins: ["jsx", "typescript"], // To allow JSX and TypeScript
Expand Down Expand Up @@ -57,7 +57,7 @@ export class JavascriptParser implements AbstractParser {
} as EnclosingContext;
}

dryRun(file: string): { valid: boolean; error: string } {
async dryRun(file: string): Promise<{ valid: boolean; error: string }> {
try {
const ast = parser.parse(file, {
sourceType: "module",
Expand Down
143 changes: 136 additions & 7 deletions src/context/language/python-parser.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,144 @@
import { AbstractParser, EnclosingContext } from "../../constants";
import Parser = require("web-tree-sitter");
let parser: Parser;

// Initialize Tree-sitter parser
async function initializeParser() {
if (!parser) {
await Parser.init();
parser = new Parser();
// You'll need to load the WASM file from your node_modules
const Lang = await Parser.Language.load('/Users/bleach/Desktop/School/Headstarter/PR Agent/SecureAgent/node_modules/tree-sitter-python/tree-sitter-python.wasm');
parser.setLanguage(Lang);
}
return parser;
}

export class PythonParser implements AbstractParser {
findEnclosingContext(
private async getParser(): Promise<Parser> {
console.log("πŸ” Getting Python parser...");
return await initializeParser();
}

async findEnclosingContext(
file: string,
lineStart: number,
lineEnd: number
): EnclosingContext {
// TODO: Implement this method for Python
return null;
): Promise<EnclosingContext> {
console.log(`πŸ” Searching for context for range: ${lineStart} - ${lineEnd}`);

const parser = await this.getParser();
const tree = parser.parse(file);

// Separate tracking for different types of contexts
const contextNodes: { definitions: Parser.SyntaxNode[]; blocks: Parser.SyntaxNode[] } = {
definitions: [], // function_definition, class_definition
blocks: [] // if_statement, with_statement, block, etc
};

// Helper to check if a node contains our target range
const nodeContainsRange = (node: Parser.SyntaxNode) => {
if (node.endPosition.row - node.startPosition.row < 1) {
return false;
}

return node.startPosition.row <= lineStart &&
node.endPosition.row >= lineEnd &&
node.text.trim().length > 0;
};

// Recursive function with context type separation
const traverseTree = (node: Parser.SyntaxNode) => {
if (node.endPosition.row - node.startPosition.row < 1) {
return;
}

if (nodeContainsRange(node)) {
const nodeType = node.type;
const nameNode = node.childForFieldName('name');
const name = nameNode ? nameNode.text : 'unnamed';

if (['function_definition', 'class_definition'].includes(nodeType)) {
console.log(`Found definition context ${nodeType}: ${name} at lines ${node.startPosition.row}-${node.endPosition.row}`);
contextNodes.definitions.push(node);
} else if (['if_statement', 'with_statement', 'block', 'while_statement', 'for_statement', 'try_statement'].includes(nodeType)) {
console.log(`Found block context ${nodeType} at lines ${node.startPosition.row}-${node.endPosition.row}`);
contextNodes.blocks.push(node);
}
}

// Only traverse children if the node might contain our range
if (node.startPosition.row <= lineEnd && node.endPosition.row >= lineStart) {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child) {
traverseTree(child);
}
}
}
};

traverseTree(tree.rootNode);

// Find the smallest containing node, prioritizing definitions
let bestNode = null;

// First try to find the smallest definition
if (contextNodes.definitions.length > 0) {
bestNode = contextNodes.definitions.reduce((smallest, current) => {
const smallestSize = smallest.endPosition.row - smallest.startPosition.row;
const currentSize = current.endPosition.row - current.startPosition.row;
return currentSize < smallestSize ? current : smallest;
});
console.log(`Selected definition context: ${bestNode.type} at lines ${bestNode.startPosition.row}-${bestNode.endPosition.row}`);
}

// If no definitions found, try blocks
if (!bestNode && contextNodes.blocks.length > 0) {
bestNode = contextNodes.blocks.reduce((largest, current) => {
const largestSize = largest.endPosition.row - largest.startPosition.row;
const currentSize = current.endPosition.row - current.startPosition.row;
return currentSize > largestSize ? current : largest;
});
console.log(`Selected block context: ${bestNode.type} at lines ${bestNode.startPosition.row}-${bestNode.endPosition.row}`);
}

if (!bestNode) {
console.log('No suitable context found');
}

return {
enclosingContext: bestNode
};
}
dryRun(file: string): { valid: boolean; error: string } {
// TODO: Implement this method for Python
return { valid: false, error: "Not implemented yet" };

async dryRun(file: string): Promise<{ valid: boolean; error: string }> {
console.log("πŸ§ͺ Starting Python parser dryRun");
try {
const parser = await this.getParser();
const tree = parser.parse(file);

// Check if there are any ERROR nodes in the syntax tree
let hasError = false;
const cursor = tree.walk();

do {
if (cursor.currentNode.type === 'ERROR') {
hasError = true;
break;
}
} while (cursor.gotoNextSibling() || cursor.gotoParent());

return {
valid: !hasError,
error: hasError ? "Syntax error in Python code" : "",
};
} catch (exc) {
console.error("❌ Python parser dryRun error:", exc);
return {
valid: false,
error: exc.toString(),
};
}
}
}
Loading