Skip to content
Merged
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
38 changes: 38 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## What does this PR do?

Explain here what you changed and why. Doesn't need to be long, just enough
for a reviewer to understand the change without reading every line of
diff. If it was a bug fix, briefly describe what was broken. Delete this paragraph and replace it with your description.

## Closes

List any issues that are linked to this pull request (e.g., `Closes #123`).

- Closes #

## Type of change(s)

- [ ] Bug fix
- [ ] New feature
- [ ] Documentation update
- [ ] Style / UX change
- [ ] Refactor (no functional change)
- [ ] Performance improvement
- [ ] Other

## Checklist

- [ ] I've tested this change locally and it works as expected
- [ ] `bun run typecheck` or `npm run typecheck` completes without errors
- [ ] My commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) (e.g. `fix: ...`, `feat: ...`, `chore: ...`)
- [ ] I've updated relevant docs (README, comments, etc.) if this change needs it
- [ ] No leftover `console.log` or debug code

## Screenshots / recordings (if applicable)

If this changes anything visual, a before/after screenshot or a short clip makes review a lot faster. Delete this section if it doesn't apply.

## Anything else the reviewer should know?

Extra context, known limitations, or specific things you'd like a
second opinion on. Delete this section if there's nothing to add.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Change Log

## `v1.2.0` - 2026-07-20

### Changed

- Replaced the previous score/regex-based estimator with a dedicated `analyzeComplexity` utility that properly tracks loop nesting depth, classifies recursion (linear / logarithmic / exponential / factorial / memoized), and produces structured indicators + reasoning. ([#41](https://github.com/open-devhub/quillbot/issues/41))

## `v1.1.1` - 2026-07-13

### Fixed

- Normalize `git://` repository URLs for Discord links in `npm` command
- Normalize `git://` repository URLs for Discord links in `npm` command ([#32](https://github.com/open-devhub/quillbot/issues/32))
- Optimize `run` command and expand its judge0 language alias map
- Harden `http` command against SSRF and DoS
- Fix reDoS risk from unrestricted user-controlled RegExp
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
"type": "module",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run --watch src/index.ts",
"format": "prettier --write .",
"check:conflicts": "bun src/scripts/conflicts.ts",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "Caleb Ephrem",
Expand Down
164 changes: 12 additions & 152 deletions src/commands/code/complexity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EmbedBuilder } from "discord.js";
import type { CommandCallbackOpts } from "../../types/command.ts";
import { analyzeComplexity } from "../../utils/analyzeComplexity.ts";
import {
parseCodeBlock,
parseCodeCommandInput,
Expand All @@ -17,7 +18,7 @@ export default {
args,
);

let code;
let code: string | undefined;

if (parsedBlock) {
code = parsedBlock.code;
Expand Down Expand Up @@ -76,8 +77,6 @@ export default {
}

const safe = (input: string, limit = 1000) => {
// if (typeof input !== "string") input = String(input);

return input.length > limit
? input.slice(0, limit) + "\n... (truncated)"
: input;
Expand All @@ -90,179 +89,40 @@ export default {
return preview.length > limit ? preview.slice(0, limit) + "..." : preview;
};

const stripDecorators = (input: string) =>
input
.replace(/\/\/.*$/gm, "")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, "");

try {
if (!code?.trim()) {
if (!code.trim()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code calls .trim() on code which is typed as string | undefined, but the optional chaining operator (?.) was removed. This will throw a runtime error if code is undefined at this point.

Confidence: 5/5

Suggested Fix
Suggested change
if (!code.trim()) {
if (!code?.trim()) {

The optional chaining operator (?.) safely handles the case where code might be undefined, preventing a "Cannot read property 'trim' of undefined" runtime error.

Prompt for AI

Copy this prompt to your AI IDE to fix this issue locally:

In src/commands/code/complexity.ts around line 93, the code calls .trim() on the
variable `code` which is typed as `string | undefined`. The original code used optional
chaining (`code?.trim()`) to safely handle the undefined case, but this was removed
during refactoring. Restore the optional chaining operator to prevent runtime errors
when code is undefined.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is already check for that 😭

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@calebephrem I encountered an error while generating a response. Please try again.

return message.reply(
"Provide code in a codeblock.\nExample:\n```js\nfor(let i=0;i<n;i++){}\n```",
);
}

const strippedCode = stripDecorators(code);
const analysis = new Set();
let score = 0;
let loopDetected = false;
let sortDetected = false;
let recursionDetected = false;
let exponentialRecursion = false;
let tripleNested = false;
let logarithmicPattern = false;
let collectionIteration = false;

const patterns = [
{
regex: /\b(?:for\s*\(|while\s*\(|do\s*\{)/g,
value: 1,
label: "Iterative loop detected",
flag: () => (loopDetected = true),
},
{
regex: /for\s*\([^)]*\)\s*\{[\s\S]*?for\s*\([^)]*\)\s*\{/g,
value: 3,
label: "Nested loops detected",
},
{
regex: /for\s*\([^)]*\)\s*\{[\s\S]*?while\s*\(/g,
value: 3,
label: "Mixed nested iteration detected",
},
{
regex:
/for\s*\([^)]*\)\s*\{[\s\S]*?for\s*\([^)]*\)\s*\{[\s\S]*?for\s*\(/g,
value: 4,
label: "Triple nested loops detected",
flag: () => (tripleNested = true),
},
{
regex: /\.sort\s*\(/g,
value: 2,
label: "Sorting operation detected",
flag: () => (sortDetected = true),
},
{
regex:
/(\/=|>>=|<<=|\*= *0\.5|Math\.log|Math\.sqrt|Math\.floor|\blog2\b|\blog10\b|n\s*\/\s*2)\b/g,
value: 0.75,
label: "Logarithmic pattern detected",
flag: () => (logarithmicPattern = true),
},
{
regex:
/\b(?:map|filter|reduce|some|every|find|includes|forEach)\s*\(/g,
value: 1,
label: "Collection iteration helper detected",
flag: () => (collectionIteration = true),
},
];

for (const pattern of patterns) {
if (pattern.regex.test(strippedCode)) {
analysis.add(pattern.label);
score += pattern.value;
if (pattern.flag) pattern.flag();
}
}

const recursionPattern =
/([a-zA-Z_$][\w$]*)\s*\([^)]*\)\s*\{([\s\S]*?)\}/g;
for (const match of strippedCode.matchAll(recursionPattern)) {
const functionName = match[1];
const functionBody = match[2];
const recursiveCalls = (
(functionBody &&
functionBody.match(new RegExp(`\\b${functionName}\\s*\\(`, "g"))) ||
[]
).length;

if (recursiveCalls >= 1) {
recursionDetected = true;
analysis.add("Recursive call pattern detected");
score += 2;
}

if (recursiveCalls > 1) {
exponentialRecursion = true;
analysis.add("Multiple recursive calls detected");
score += 3;
}
}

if (analysis.size === 0) {
analysis.add("No strong complexity indicators detected");
}

const complexity = (() => {
if (exponentialRecursion) return "O(2ⁿ)";
if (tripleNested) return "O(n³)";
if (
analysis.has("Nested loops detected") ||
analysis.has("Mixed nested iteration detected")
)
return "O(n²)";
if (sortDetected) return "O(n log n)";
if (logarithmicPattern && !loopDetected && !recursionDetected)
return "O(log n)";
if (recursionDetected) return "O(n)";
if (loopDetected || collectionIteration) return "O(n)";
return "O(1)";
})();

const confidence = score >= 5 ? "High" : score >= 2 ? "Medium" : "Low";
const reasoning = [];
if (exponentialRecursion)
reasoning.push(
"Multiple recursive calls often indicate exponential growth.",
);
else if (tripleNested)
reasoning.push("Triple nested loops strongly suggest cubic time.");
else if (
analysis.has("Nested loops detected") ||
analysis.has("Mixed nested iteration detected")
)
reasoning.push("Nested iteration is the primary driver of complexity.");
else if (sortDetected)
reasoning.push("Sorting operations are typically O(n log n).");
else if (logarithmicPattern)
reasoning.push(
"Logarithmic operations can indicate O(log n) behavior.",
);
else if (recursionDetected)
reasoning.push(
"Single recursive call patterns often map to linear recursion.",
);
else if (loopDetected || collectionIteration)
reasoning.push("Linear iteration is the main complexity indicator.");
else reasoning.push("No clear complexity-driving patterns found.");
const result = analyzeComplexity(code);

const embed = new EmbedBuilder()
.setTitle("Big-O Complexity Estimation")
.setColor(0x5865f2)
.addFields(
{
name: "Estimated Complexity",
value: `\`${complexity}\``,
value: `\`${result.complexity}\``,
inline: true,
},
{
name: "Confidence",
value: confidence,
value: result.confidence,
inline: true,
},
{
name: "Key Indicators",
value: Array.from(analysis)
.slice(0, 5)
.map((x) => `• ${x}`)
.join("\n"),
value:
result.indicators
.slice(0, 5)
.map((x) => `• ${x}`)
.join("\n") || "• None",
},
{
name: "Reasoning",
value: reasoning.join(" "),
value: result.reasoning.join(" ") || "No additional reasoning.",
},
{
name: "Code Preview",
Expand Down
Loading
Loading