-
Notifications
You must be signed in to change notification settings - Fork 3
feat: refactor architecture to identify snippets #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a0bd454
feat: new prisma schema
mattiacerutti 926008f
feat: add new migration
mattiacerutti 3f5de5d
feat: add file seeding
mattiacerutti 8d81f8a
feat: add functioning new architecture
mattiacerutti 030b49f
refactor: cleanup
mattiacerutti 7185a0d
feat: add init migration
mattiacerutti 67c87ab
refactor: add helper function for github api
mattiacerutti c390b12
fix: fix tabs in between filter
mattiacerutti c7d4ba3
fix: prevent error on seed when there are no unparsed files
mattiacerutti 62e4b78
feat: add onDelete behavior for foreign keys
mattiacerutti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| const GITHUB_API_URL = "https://api.github.com"; | ||
| const RETRY_DELAY_MS = 1000; | ||
| const RATE_LIMIT_PADDING_MS = 3000; | ||
|
|
||
| type GitHubRequestParams = Record<string, string | number | boolean | undefined>; | ||
|
|
||
| export interface GitHubRequestOptions extends RequestInit { | ||
| params?: GitHubRequestParams; | ||
| maxAttempts?: number; | ||
| retry?: boolean; | ||
| shouldRetry?: (context: {response: Response; attempt: number; maxAttempts: number; url: string}) => Promise<boolean> | boolean; | ||
| } | ||
|
|
||
| export async function callGitHubApi(path: string, options: GitHubRequestOptions = {}): Promise<Response> { | ||
| const token = getToken(); | ||
| const {params, maxAttempts = 3, retry = true, shouldRetry, headers: customHeaders, ...fetchOptions} = options; | ||
|
|
||
| const url = buildGitHubUrl(path, params); | ||
| const headers = new Headers({ | ||
| Authorization: `Bearer ${token}`, | ||
| Accept: "application/vnd.github+json", | ||
| }); | ||
|
|
||
| if (customHeaders) { | ||
| new Headers(customHeaders).forEach((value, key) => { | ||
| headers.set(key, value); | ||
| }); | ||
| } | ||
|
|
||
| const attempts = retry ? maxAttempts : 1; | ||
|
|
||
| for (let attempt = 1; attempt <= attempts; attempt += 1) { | ||
| const response = await fetch(url, { | ||
| ...fetchOptions, | ||
| headers, | ||
| }); | ||
|
|
||
| if (isRateLimitStatus(response.status)) { | ||
| await handleRateLimit(response); | ||
| continue; | ||
| } | ||
|
|
||
| if (!response.ok) { | ||
| if (attempt >= attempts) { | ||
| throw new Error(`GitHub API request failed for ${url} (attempt ${attempt}/${attempts}) with status ${response.status}: ${response.statusText}`); | ||
| } | ||
|
|
||
| await wait(RETRY_DELAY_MS); | ||
| continue; | ||
| } | ||
|
|
||
| if (retry && shouldRetry && attempt < attempts) { | ||
| const shouldRetryResult = await shouldRetry({ | ||
| response: response.clone(), | ||
| attempt, | ||
| maxAttempts, | ||
| url, | ||
| }); | ||
|
|
||
| if (shouldRetryResult) { | ||
| await wait(RETRY_DELAY_MS); | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| return response; | ||
| } | ||
|
|
||
| throw new Error(`GitHub API request failed after ${attempts} attempts for ${url}`); | ||
| } | ||
|
|
||
| function getToken(): string { | ||
| const token = process.env.GITHUB_API_TOKEN; | ||
|
|
||
| if (!token) { | ||
| throw new Error("Missing GITHUB_API_TOKEN environment variable"); | ||
| } | ||
|
|
||
| return token; | ||
| } | ||
|
|
||
| function buildGitHubUrl(path: string, params?: GitHubRequestParams): string { | ||
| const baseUrl = path.startsWith("http") ? new URL(path) : new URL(path.startsWith("/") ? path : `/${path}`, GITHUB_API_URL); | ||
|
|
||
| if (params) { | ||
| Object.entries(params).forEach(([key, value]) => { | ||
| if (value === undefined) return; | ||
| baseUrl.searchParams.set(key, String(value)); | ||
| }); | ||
| } | ||
|
|
||
| return baseUrl.toString(); | ||
| } | ||
|
|
||
| function isRateLimitStatus(status: number) { | ||
| return status === 403 || status === 429; | ||
| } | ||
|
|
||
| async function handleRateLimit(response: Response): Promise<void> { | ||
| const resetAt = getRateLimitReset(response); | ||
| if (resetAt) { | ||
| const waitTime = Math.max(resetAt - Date.now(), 0) + RATE_LIMIT_PADDING_MS; | ||
| console.warn(`Rate limited by GitHub (status ${response.status}). Waiting ${waitTime}ms until reset.`); | ||
| await wait(waitTime); | ||
| return; | ||
| } | ||
|
|
||
| const retryAfter = parseRetryAfter(response.headers.get("Retry-After")); | ||
| if (retryAfter !== null) { | ||
| console.warn(`Rate limited by GitHub (status ${response.status}). Waiting ${retryAfter}ms due to Retry-After header.`); | ||
| await wait(retryAfter); | ||
| return; | ||
| } | ||
|
|
||
| console.warn(`Rate limited by GitHub (status ${response.status}). Waiting ${RETRY_DELAY_MS}ms before retrying.`); | ||
| await wait(RETRY_DELAY_MS); | ||
| } | ||
|
|
||
| function getRateLimitReset(response: Response): number | null { | ||
| const headerValue = response.headers.get("X-RateLimit-Reset"); | ||
| if (!headerValue) return null; | ||
|
|
||
| const epochSeconds = Number(headerValue); | ||
| if (Number.isNaN(epochSeconds)) { | ||
| return null; | ||
| } | ||
|
|
||
| return epochSeconds * 1000; | ||
| } | ||
|
|
||
| function parseRetryAfter(headerValue: string | null): number | null { | ||
| if (!headerValue) return null; | ||
|
|
||
| const numericValue = Number(headerValue); | ||
| if (!Number.isNaN(numericValue)) { | ||
| return numericValue * 1000; | ||
| } | ||
|
|
||
| const retryDate = new Date(headerValue); | ||
| const waitTime = retryDate.getTime() - Date.now(); | ||
| return Number.isNaN(waitTime) ? null : Math.max(waitTime, 0); | ||
| } | ||
|
|
||
| function wait(duration: number) { | ||
| return new Promise((resolve) => setTimeout(resolve, duration)); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| export const MAX_LINES = 11; | ||
| export const MIN_LINES = 3; | ||
|
|
||
| export const MAX_LINE_LENGTH = 72; | ||
|
|
||
| export const MAX_SNIPPET_LENGTH = 350; | ||
| export const MIN_SNIPPET_LENGTH = 75; | ||
|
|
||
| export const VALID_NODES = [ | ||
| "function_declaration", | ||
| "function_definition", | ||
| "class_declaration", | ||
| "class_definition", | ||
| "function_item", | ||
| "method_declaration", | ||
| "method_definition", | ||
| "method_item", | ||
| "class_item", | ||
| "interface_declaration", | ||
| "interface_definition", | ||
| "interface_item", | ||
| "module", | ||
| "switch_statement", | ||
| "while_statement", | ||
| "for_statement", | ||
| "if_statement", | ||
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import {extractSnippets} from "./parsing/parser"; | ||
| import {filterSnippets} from "./processing/filter"; | ||
| import {formatCode} from "./processing/process"; | ||
| import {hashSnippet} from "./processing/utils"; | ||
|
|
||
| interface ISnippetOutput { | ||
| sha: string; | ||
| content: string; | ||
| rawContent: string; | ||
| } | ||
|
|
||
| export function getSnippetsFromFile(fileContent: string, languageId: string): ISnippetOutput[] { | ||
| const extractedSnippets = extractSnippets(fileContent, languageId); | ||
|
|
||
| const formattedSnippets = extractedSnippets | ||
| .map((snippet) => ({ | ||
| sha: hashSnippet(snippet), | ||
| content: formatCode(snippet), | ||
| rawContent: snippet, | ||
| })) | ||
| .filter((s) => s.content !== null) as ISnippetOutput[]; | ||
|
|
||
| const filteredSnippets = formattedSnippets.filter((snippet) => filterSnippets(snippet.content)); | ||
|
|
||
| return filteredSnippets; | ||
| } |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import {getInitialIndentation} from "@lib/snippets/processing/indentation"; | ||
| import {getTSParser} from "./parser-factory"; | ||
| import IParser from "tree-sitter"; | ||
| import {isValidNode} from "@lib/snippets/processing/filter"; | ||
|
|
||
| export function findValidNodes(node: IParser.SyntaxNode): IParser.SyntaxNode[] { | ||
| let nodes: IParser.SyntaxNode[] = []; | ||
| if (isValidNode(node)) { | ||
| nodes.push(node); | ||
| } | ||
|
|
||
| node.children.forEach((child) => { | ||
| if (child === null) return; | ||
| nodes = nodes.concat(findValidNodes(child)); | ||
| }); | ||
|
|
||
| return nodes; | ||
| } | ||
|
|
||
| export function convertSnippetToText(node: IParser.SyntaxNode, sourceCode: string): string { | ||
| const initialIndentation = getInitialIndentation(node.startIndex, sourceCode); | ||
|
|
||
| return initialIndentation + getNodeText(node); | ||
| } | ||
|
|
||
| export function extractSnippets(fileContent: string, languageId: string): string[] { | ||
| const parser = getTSParser(languageId); | ||
|
|
||
| let parsedCode: IParser.Tree; | ||
|
|
||
| try { | ||
| parsedCode = parser.parse(fileContent); | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| } catch (error) { | ||
| return []; | ||
| } | ||
|
|
||
| const validNodes = findValidNodes(parsedCode.rootNode); | ||
| const validSnippets = validNodes.map((node) => convertSnippetToText(node, fileContent)); | ||
|
|
||
| return validSnippets; | ||
| } | ||
|
|
||
| export function getNodeText(node: IParser.SyntaxNode): string { | ||
| return node.text; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import IParser from "tree-sitter"; | ||
| import {MAX_LINE_LENGTH, MAX_LINES, MAX_SNIPPET_LENGTH, MIN_LINES, MIN_SNIPPET_LENGTH, VALID_NODES} from "../config"; | ||
|
|
||
| function filterLinesNumber(snippet: string): boolean { | ||
| const numLines = snippet.split("\n").length; | ||
| return numLines <= MAX_LINES && numLines >= MIN_LINES; | ||
| } | ||
|
|
||
| function filterLineLength(snippet: string): boolean { | ||
| const lines = snippet.split("\n"); | ||
| return lines.every((line) => line.trim().length <= MAX_LINE_LENGTH); | ||
| } | ||
|
|
||
| function filterSnippetLength(snippet: string): boolean { | ||
| return snippet.length >= MIN_SNIPPET_LENGTH && snippet.length <= MAX_SNIPPET_LENGTH; | ||
| } | ||
|
|
||
| function filterSnippetSpecialCharacters(snippet: string): boolean { | ||
| const regex = /^[\x00-\x7F]+$/; | ||
| const test = regex.test(snippet); | ||
|
|
||
| return test; | ||
| } | ||
|
|
||
| function filterTabsInBetween(snippet: string): boolean { | ||
| const regex = /^.+\\t.+$/; | ||
| return !regex.test(snippet); | ||
| } | ||
|
|
||
| export function isValidNode(node: IParser.SyntaxNode) { | ||
| return VALID_NODES.includes(node.type); | ||
| } | ||
|
|
||
| export function filterSnippets(content: string): boolean { | ||
| return filterSnippetLength(content) && filterLinesNumber(content) && filterLineLength(content) && filterSnippetSpecialCharacters(content) && filterTabsInBetween(content); | ||
| } | ||
2 changes: 1 addition & 1 deletion
2
...ts/logic/processing/indentation.server.ts → lib/snippets/processing/indentation.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import {adjustIndentationOffset, applyIndentationToEmptyLines, detectIndentationStyle} from "./indentation"; | ||
| import {countInitialWhitespaces} from "./utils"; | ||
|
|
||
| function cleanText(text: string): string { | ||
| // Remove whitespaces from empty lines | ||
| text = text.replace(/(?<=\n)[ \t]+(?=\n)/g, ""); | ||
|
|
||
| return text; | ||
| } | ||
|
|
||
| function hasProhibitedCharacters(input: string): boolean { | ||
| const regex = /((?:(?![ \t\n])\s)|\\)/; | ||
| return regex.test(input); | ||
| } | ||
|
|
||
| export function formatCode(snippet: string): string | null { | ||
| if (hasProhibitedCharacters(snippet)) { | ||
| return null; | ||
| } | ||
|
|
||
| // Clean raw snippet | ||
| snippet = cleanText(snippet); | ||
|
|
||
| const indentationStyle = detectIndentationStyle(snippet); | ||
|
|
||
| if (indentationStyle.type === "mixed") { | ||
| return null; | ||
| } | ||
|
|
||
| if (indentationStyle.type === "none") { | ||
| return snippet; | ||
| } | ||
|
|
||
| // Convert raw snippet indentation style with standardized '\t' indentation | ||
| let codeLines = snippet.split("\n").map((line) => { | ||
| const initialWhitespaces = countInitialWhitespaces(line); | ||
|
|
||
| const trimmedLine = line.trim(); | ||
|
|
||
| if (indentationStyle.type === "space") { | ||
| return "\t".repeat(indentationStyle.value.indexOf(initialWhitespaces)) + trimmedLine; | ||
| } | ||
|
|
||
| return "\t".repeat(initialWhitespaces) + trimmedLine; | ||
| }); | ||
|
|
||
| // Adjust indentation offset on every line | ||
| codeLines = adjustIndentationOffset(codeLines); | ||
|
|
||
| // Applies indentation to empty lines (the minimum indentation of the surrounding lines) | ||
| codeLines = applyIndentationToEmptyLines(codeLines); | ||
|
|
||
| return codeLines.join("\n"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
ASCII-only regex triggers Biome warning and could be modernized
filterSnippetSpecialCharactersuses/^[\x00-\x7F]+$/, which Biome flags (noControlCharactersInRegex) and enforces strict ASCII-only content. If you want “ASCII-only” semantics, you can avoid explicit control-character ranges and satisfy the linter with Unicode properties:Also double‑check that rejecting any non‑ASCII characters (identifiers, comments, emojis, etc.) is intentional, as this will exclude many valid snippets.
🏁 Script executed:
Length of output: 1255
🏁 Script executed:
Length of output: 12
🏁 Script executed:
Length of output: 637
🏁 Script executed:
Length of output: 746
🏁 Script executed:
Length of output: 0
🌐 Web query:
💡 Result:
Yes — JavaScript (and therefore TypeScript at runtime) supports the Unicode binary property ASCII via Unicode property escapes. Use the u (Unicode) flag, e.g. /\p{ASCII}/u. [1][2]
Sources:
[1] MDN — Unicode character class escape (\p{...}, \P{...}).
[2] MDN (Chinese) — notes listing binary properties including ASCII.
🌐 Web query:
💡 Result:
The Biome rule noControlCharactersInRegex flags control (non-printable) ASCII characters in regular expressions — i.e. characters U+0000–U+001F — when they appear as:
It allows common control escape sequences like \t and \n. [1][2]
Length of output: 1774
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 3868
🏁 Script executed:
Length of output: 594
The suggested Unicode property syntax may not work with your ES2017 TypeScript target without additional transpilation config
The Biome warning (noControlCharactersInRegex) is real and correctly triggered by
/^[\x00-\x7F]+$/. However, the suggested fix/^[\p{ASCII}]+$/uuses Unicode properties, which were added in ES2018—your tsconfig targets ES2017.To satisfy Biome without risking transpilation issues, simplify the regex to avoid explicit control-character ranges:
function filterSnippetSpecialCharacters(snippet: string): boolean { - const regex = /^[\x00-\x7F]+$/; - const test = regex.test(snippet); - - return test; + // Allow only printable ASCII characters (0x20–0x7E). + const regex = /^[ -~]*$/; + return regex.test(snippet); }This avoids control characters entirely and satisfies Biome without transpilation concerns. Alternatively, confirm your build system handles Unicode property transpilation, then the
/^\p{ASCII}+$/uapproach works.Also verify that rejecting all non-ASCII content (identifiers, comments, emojis, etc.) is intentional—this filter may be too restrictive for valid snippets.
🧰 Tools
🪛 Biome (2.1.2)
[error] 19-19: Unexpected control character in a regular expression.
Control characters are unusual and potentially incorrect inputs, so they are disallowed.
(lint/suspicious/noControlCharactersInRegex)
🤖 Prompt for AI Agents