Skip to content

Commit 31deb47

Browse files
committed
Add option to resolve issues
1 parent 2791e12 commit 31deb47

File tree

5 files changed

+308
-115
lines changed

5 files changed

+308
-115
lines changed

Diff for: .vscode/launch.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"<node_internals>/**"
2525
],
2626
"program": "${workspaceFolder}/index.js",
27-
"args": ["${userHome}/Repositories/sebromero/Arduino_NiclaLamon/src", "-f"]
27+
"args": ["${userHome}/Repositories/arduino-libraries/Arduino_UnifiedStorage/src/UFile.h", "-f", "-r"]
2828
}
2929
]
3030
}

Diff for: index.js

+32-113
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,19 @@ import moxygen from "moxygen";
44
import assign from "object-assign";
55
import { program } from "commander";
66
import fs from "fs";
7-
import doxygen from "doxygen";
7+
import DoxygenRunner from "./doxygen-runner.js";
88
import { fileURLToPath } from 'url';
99
import { dirname } from 'path';
1010
import path from "path";
11+
import IssueResolver from "./issue-resolver.js";
12+
import { createDirectories } from "./helpers.js";
1113

1214
const __filename = fileURLToPath(import.meta.url);
1315
const __dirname = dirname(__filename);
1416

1517
const TEMPLATES_FOLDER = path.join(__dirname, "templates/cpp")
16-
const XML_FOLDER = "./build/xml/"
1718
const PROGRAMMING_LANGUAGE = "cpp"
18-
const DOXYGEN_FILE_PATH = "./doxygen.config"
19-
20-
/**
21-
* Creates directories if they do not exist.
22-
* @param {string[]} dirs - An array of directory paths.
23-
*/
24-
const createDirectories = (dirs) => {
25-
for (const dir of dirs) {
26-
if (!fs.existsSync(dir)) {
27-
fs.mkdirSync(dir, { recursive: true })
28-
}
29-
}
30-
}
31-
32-
/**
33-
* Cleans the specified directory by removing all files and subdirectories recursively.
34-
* @param {string} dir - The directory path to be cleaned.
35-
*/
36-
const cleanDirectory = (dir) => {
37-
if (fs.existsSync(dir)) {
38-
const files = fs.readdirSync(dir)
39-
for (const file of files) {
40-
const path = dir + "/" + file
41-
const stat = fs.statSync(path)
42-
if (stat && stat.isDirectory()) {
43-
cleanDirectory(path)
44-
fs.rmdirSync(path)
45-
} else {
46-
fs.unlinkSync(path)
47-
}
48-
}
49-
}
50-
}
19+
const XML_FOLDER = "./build/xml/"
5120

5221
// Extract the command version from the package.json file
5322
const version = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'))).version;
@@ -65,6 +34,7 @@ program.option('-c, --include-cpp', 'Process .cpp files when rendering the docum
6534
program.option('-a, --access-level <string>', 'Minimum access level to be considered (public, private)', "public")
6635
program.option('-f, --fail-on-warnings', 'Fail when undocumented code is found', false)
6736
program.option('-d, --debug', 'Enable debugging mode with additional output.', false)
37+
program.option('-r, --resolve-issues', 'Automatically fix issues in the documentation', false)
6838

6939
if (process.argv.length < 2) {
7040
program.help();
@@ -83,95 +53,37 @@ if (includeCppFiles) {
8353
fileExtensions.push("*.cpp")
8454
}
8555

86-
if(outputXML){
87-
cleanDirectory("./build")
88-
createDirectories(["./build"])
89-
}
90-
91-
// Check if output path exists. If not, create it.
92-
if(outputFile){
93-
const outputFolder = path.dirname(outputFile)
94-
createDirectories([outputFolder])
95-
}
96-
97-
if(!doxygen.isDoxygenExecutableInstalled()) {
98-
console.log(`Doxygen is not installed. Downloading ...`)
99-
const success = await doxygen.downloadVersion();
100-
if (!success) {
101-
console.error("Failed to download Doxygen")
102-
process.exit(1)
56+
try {
57+
const options = {
58+
"outputXML": outputXML,
59+
"xmlFolder": XML_FOLDER,
60+
"sourceFolder": sourceFolder,
61+
"fileExtensions": fileExtensions,
62+
"exclude": commandOptions.exclude,
63+
"accessLevel": commandOptions.accessLevel,
64+
"failOnWarnings": commandOptions.failOnWarnings,
65+
"debug": commandOptions.debug
10366
}
104-
}
105-
106-
// The configuration options for Doxygen
107-
const doxyFileOptions = {
108-
INPUT: sourceFolder,
109-
RECURSIVE: "YES",
110-
GENERATE_HTML: "NO",
111-
GENERATE_LATEX: "NO",
112-
GENERATE_XML: outputXML ? "YES" : "NO", // XML output is required for moxygen
113-
XML_OUTPUT: XML_FOLDER,
114-
CASE_SENSE_NAMES: "NO", // Creates case insensitive links compatible with GitHub
115-
INCLUDE_FILE_PATTERNS: fileExtensions.join(" "),
116-
EXCLUDE_PATTERNS: commandOptions.exclude ? commandOptions.exclude : "",
117-
EXTRACT_PRIVATE: commandOptions.accessLevel === "private" ? "YES" : "NO",
118-
EXTRACT_STATIC: "NO",
119-
QUIET: commandOptions.debug ? "NO" : "YES",
120-
WARN_NO_PARAMDOC: "YES", // Warn if a parameter is not documented
121-
WARN_AS_ERROR: commandOptions.failOnWarnings ? "FAIL_ON_WARNINGS" : "NO", // Treat warnings as errors. Continues if warnings are found.
122-
}
123-
124-
if(commandOptions.debug) console.log(`🔧 Creating Doxygen config file ${DOXYGEN_FILE_PATH} ...`)
125-
doxygen.createConfig(doxyFileOptions, DOXYGEN_FILE_PATH)
67+
const doxygenRunner = new DoxygenRunner(options)
68+
await doxygenRunner.run()
12669

127-
try {
128-
if(commandOptions.debug) console.log("🏃 Running Doxygen ...")
129-
if(doxyFileOptions.GENERATE_XML === "YES") {
130-
console.log(`🔨 Generating XML documentation at ${XML_FOLDER} ...`)
131-
}
132-
doxygen.run(DOXYGEN_FILE_PATH)
13370
} catch (error) {
134-
// Replace all "\n " with " " to meld the error messages into one line
135-
let errorMessages = error.stderr.toString().replace(/\n /g, " ").split("\n")
136-
137-
// Filter out empty messages and allow only warnings related to documentation issues
138-
const filteredMessages = errorMessages.filter(message => {
139-
const warningMessageRegex = /^(?:[^:\n]+):(?:\d+): warning: (?:.+)$/
140-
return message.match(warningMessageRegex)
141-
})
142-
143-
if(commandOptions.debug){
144-
// Print messages that were not filtered out and are not empty
145-
const remainingMessages = errorMessages.filter(message => {
146-
return !filteredMessages.includes(message) && message !== ""
147-
})
148-
for (const message of remainingMessages) {
149-
console.warn(`🤔 ${message}`)
150-
}
151-
}
71+
const validationMessages = error.messages
15272

153-
if(filteredMessages.length > 0 && commandOptions.failOnWarnings) {
73+
if(validationMessages && commandOptions.failOnWarnings) {
15474
console.error("❌ Issues in the documentation were found.")
155-
for (const message of filteredMessages) {
75+
if(commandOptions.resolveIssues){
76+
console.log("🔨 Resolving issues ...")
77+
const resolver = new IssueResolver(validationMessages, "<key>")
78+
await resolver.resolve()
79+
}
80+
for (const message of validationMessages) {
15681
console.warn(`😬 ${message}`)
15782
}
15883
process.exit(1)
15984
}
16085
}
16186

162-
if(outputXML){
163-
const xmlFiles = fs.readdirSync(XML_FOLDER)
164-
if (xmlFiles.length === 0) {
165-
console.error(`❌ No XML files found in ${XML_FOLDER}.`)
166-
process.exit(1)
167-
} else if(commandOptions.debug){
168-
console.log(`✅ Found ${xmlFiles.length} XML files.`)
169-
for (const file of xmlFiles) {
170-
console.log(`📄 ${file}`)
171-
}
172-
}
173-
}
174-
17587
// The configuration options for moxygen
17688
const moxygenOptions = {
17789
quiet: true, /** Do not output anything to the console **/
@@ -201,6 +113,13 @@ const finalMoxygenOptions = assign({}, moxygen.defaultOptions, {
201113
});
202114

203115
if(outputXML){
116+
// Check if output path exists. If not, create it.
117+
if(outputFile){
118+
const outputFolder = path.dirname(outputFile)
119+
if(commandOptions.debug) console.log(`🔧 Creating output directory ${outputFolder} ...`)
120+
createDirectories([outputFolder])
121+
}
122+
204123
moxygen.logger.init(finalMoxygenOptions);
205124
console.log("🔨 Generating markdown documentation...")
206125
moxygen.run(finalMoxygenOptions);

Diff for: issue-resolver.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import fs from "fs"
2+
import path from "path"
3+
import OpenAI from 'openai'
4+
5+
class IssueResolver {
6+
constructor(messages, apiKey = process.env["OPENAI_API_KEY"]) {
7+
this.openai = new OpenAI({
8+
apiKey: apiKey
9+
});
10+
11+
this.messages = this.groupMessagesByFile(messages)
12+
}
13+
14+
groupMessagesByFile(messages){
15+
return messages.reduce((accumulator, message) => {
16+
// Messages are in the format "file:line: warning: message"
17+
const [file, line, warning, ...rest] = message.split(":")
18+
const key = `${file}`
19+
if(!accumulator[key]) accumulator[key] = []
20+
accumulator[key].push(`${line}: ${rest.join(":").trim()}`)
21+
return accumulator
22+
}, {})
23+
}
24+
25+
constructPrompt(file, messages){
26+
console.log(`📄 Constructing prompt for file ${file}`)
27+
const fileContents = fs.readFileSync(file).toString()
28+
const filename = path.basename(file)
29+
const promptIntro = `There are some issues with the documentation in a C++ file (${filename}). Here is the code:\n\n\`\`\`cpp\n${fileContents}\n\`\`\``
30+
const promptWarnings = `The following are the warnings generated by doxygen. They are in the format 'line: warning': \n\n\`\`\`\n${messages.join("\n")}\n\`\`\``
31+
const promptOutro = `Could you please fix the issues? Please print the whole file including those fixes as a code block without any extra text or explanations.`
32+
const constructedPrompt = `${promptIntro}\n\n${promptWarnings}\n\n${promptOutro}`
33+
return constructedPrompt
34+
}
35+
36+
async getResolvedFile(file){
37+
const messages = this.messages[file]
38+
const prompt = this.constructPrompt(file, messages)
39+
try {
40+
const chatCompletion = await this.openai.chat.completions.create({
41+
messages: [{ role: 'user', content: prompt }],
42+
model: 'gpt-3.5-turbo',
43+
});
44+
console.log(chatCompletion)
45+
const response = chatCompletion.data.choices[0].text
46+
return response
47+
} catch (error) {
48+
console.error(`❌ Error resolving issues. ${error.message}`)
49+
return null
50+
}
51+
}
52+
53+
async resolve() {
54+
let result = true
55+
56+
for (const file of Object.keys(this.messages)) {
57+
const resolvedFile = await this.getResolvedFile(file)
58+
if(resolvedFile){
59+
fs.writeFileSync(file, resolvedFile)
60+
} else {
61+
result = false
62+
}
63+
}
64+
return result
65+
}
66+
}
67+
68+
export default IssueResolver

0 commit comments

Comments
 (0)