Skip to content

Commit ba514f8

Browse files
committed
feat: Implement markdown, YAML, and JSON file linting
1 parent 9edf11f commit ba514f8

File tree

1 file changed

+189
-11
lines changed

1 file changed

+189
-11
lines changed

linters/typescript/typescript_linter.ts

Lines changed: 189 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,124 @@ AI Context Convention TypeScript Linter (v${packageVersion})
7474
}
7575

7676
private lintMarkdownFile(content: string): void {
77-
// TODO: Implement markdown file linting
7877
console.log(' - Validating Markdown structure');
7978
console.log(' - Checking YAML frontmatter');
8079
console.log(' - Verifying content against AI Context Convention Specification');
80+
81+
// Split content into frontmatter and markdown
82+
const parts = content.split('---\n');
83+
if (parts.length < 3) {
84+
console.error(' Error: Invalid markdown structure. YAML frontmatter is missing or incomplete.');
85+
return;
86+
}
87+
88+
const frontmatter = parts[1];
89+
const markdownContent = parts.slice(2).join('---\n').trim();
90+
91+
// Parse and validate frontmatter
92+
try {
93+
const frontmatterData = yaml.load(frontmatter) as Record<string, unknown>;
94+
this.validateContextData(frontmatterData);
95+
} catch (error) {
96+
console.error(` Error parsing YAML frontmatter: ${error}`);
97+
}
98+
99+
// Validate markdown content
100+
this.validateMarkdownContent(markdownContent);
101+
}
102+
103+
private validateContextData(data: Record<string, unknown>): void {
104+
const requiredFields = ['directoryName', 'description'];
105+
for (const field of requiredFields) {
106+
if (!(field in data)) {
107+
console.error(` Error: Missing required field '${field}'.`);
108+
}
109+
}
110+
111+
if ('language' in data && typeof data.language !== 'string') {
112+
console.error(' Error: Field \'language\' must be a string.');
113+
}
114+
115+
if ('dependencies' in data && !Array.isArray(data.dependencies)) {
116+
console.error(' Error: Field \'dependencies\' must be an array.');
117+
}
118+
119+
// Add more specific checks as needed
120+
}
121+
122+
private validateMarkdownContent(content: string): void {
123+
const tokens = this.md.parse(content, {});
124+
let hasTitle = false;
125+
let hasDescriptionSection = false;
126+
let hasFileStructureSection = false;
127+
let hasUsageSection = false;
128+
let currentSection = '';
129+
130+
for (let i = 0; i < tokens.length; i++) {
131+
const token = tokens[i];
132+
133+
if (token.type === 'heading_open') {
134+
if (token.tag === 'h1' && !hasTitle) {
135+
hasTitle = true;
136+
} else if (token.tag === 'h2') {
137+
currentSection = tokens[i + 1].content.toLowerCase();
138+
if (currentSection === 'description') hasDescriptionSection = true;
139+
if (currentSection === 'file structure') hasFileStructureSection = true;
140+
if (currentSection === 'usage') hasUsageSection = true;
141+
}
142+
}
143+
144+
if (token.type === 'link_open') {
145+
const hrefToken = tokens[i + 1];
146+
if (hrefToken.type !== 'text' || !hrefToken.content.startsWith('http')) {
147+
console.error(' Warning: Link may be improperly formatted or using relative path.');
148+
}
149+
}
150+
151+
if (token.type === 'fence' && !token.info) {
152+
console.error(' Warning: Code block is missing language specification.');
153+
}
154+
}
155+
156+
if (!hasTitle) {
157+
console.error(' Error: Markdown content should start with a title (H1 heading).');
158+
}
159+
160+
if (!hasDescriptionSection) {
161+
console.error(' Warning: Markdown content is missing a "Description" section.');
162+
}
163+
164+
if (!hasFileStructureSection) {
165+
console.warn(' Note: Consider adding a "File Structure" section for better context.');
166+
}
167+
168+
if (!hasUsageSection) {
169+
console.warn(' Note: Consider adding a "Usage" section for better understanding.');
170+
}
81171
}
82172

83173
private lintYamlFile(content: string): void {
84-
// TODO: Implement YAML file linting
85174
console.log(' - Validating YAML structure');
86175
console.log(' - Verifying content against AI Context Convention Specification');
176+
177+
try {
178+
const yamlData = yaml.load(content) as Record<string, unknown>;
179+
this.validateContextData(yamlData);
180+
} catch (error) {
181+
console.error(` Error parsing YAML file: ${error}`);
182+
}
87183
}
88184

89185
private lintJsonFile(content: string): void {
90-
// TODO: Implement JSON file linting
91186
console.log(' - Validating JSON structure');
92187
console.log(' - Verifying content against AI Context Convention Specification');
188+
189+
try {
190+
const jsonData = JSON.parse(content) as Record<string, unknown>;
191+
this.validateContextData(jsonData);
192+
} catch (error) {
193+
console.error(` Error parsing JSON file: ${error}`);
194+
}
93195
}
94196

95197
private lintContextdocsFile(filePath: string): void {
@@ -100,10 +202,54 @@ AI Context Convention TypeScript Linter (v${packageVersion})
100202
console.log(' - Checking for required sections');
101203
console.log(' - Verifying content against .contextdocs.md specification');
102204

103-
// TODO: Implement specific checks for .contextdocs.md
104-
// 1. Check if the file starts with a level 1 heading "AI Context Documentation"
105-
// 2. Verify that all required sections are present (e.g., "Overview", "File Structure", "Conventions")
106-
// 3. Ensure that the content follows the expected format for each section
205+
const tokens = this.md.parse(content, {});
206+
let hasTitle = false;
207+
let hasOverview = false;
208+
let hasFileStructure = false;
209+
let hasConventions = false;
210+
let currentSection = '';
211+
212+
for (let i = 0; i < tokens.length; i++) {
213+
const token = tokens[i];
214+
215+
if (token.type === 'heading_open') {
216+
if (token.tag === 'h1' && !hasTitle) {
217+
hasTitle = tokens[i + 1].content === 'AI Context Documentation';
218+
} else if (token.tag === 'h2') {
219+
currentSection = tokens[i + 1].content.toLowerCase();
220+
if (currentSection === 'overview') hasOverview = true;
221+
if (currentSection === 'file structure') hasFileStructure = true;
222+
if (currentSection === 'conventions') hasConventions = true;
223+
}
224+
}
225+
226+
if (token.type === 'link_open') {
227+
const hrefToken = tokens[i + 1];
228+
if (hrefToken.type !== 'text' || !hrefToken.content.startsWith('http')) {
229+
console.error(' Warning: Link may be improperly formatted or using relative path.');
230+
}
231+
}
232+
233+
if (token.type === 'fence' && !token.info) {
234+
console.error(' Warning: Code block is missing language specification.');
235+
}
236+
}
237+
238+
if (!hasTitle) {
239+
console.error(' Error: .contextdocs.md should start with the title "AI Context Documentation".');
240+
}
241+
242+
if (!hasOverview) {
243+
console.error(' Error: .contextdocs.md is missing the "Overview" section.');
244+
}
245+
246+
if (!hasFileStructure) {
247+
console.error(' Error: .contextdocs.md is missing the "File Structure" section.');
248+
}
249+
250+
if (!hasConventions) {
251+
console.error(' Error: .contextdocs.md is missing the "Conventions" section.');
252+
}
107253
}
108254

109255
private lintContextignoreFile(filePath: string): void {
@@ -113,10 +259,42 @@ AI Context Convention TypeScript Linter (v${packageVersion})
113259
console.log(' - Validating .contextignore format');
114260
console.log(' - Checking for valid ignore patterns');
115261

116-
// TODO: Implement specific checks for .contextignore
117-
// 1. Verify that each line is a valid ignore pattern (similar to .gitignore format)
118-
// 2. Check for any conflicting or redundant patterns
119-
// 3. Ensure that the file doesn't ignore critical context files (e.g., .context.md files)
262+
const lines = content.split('\n').map(line => line.trim()).filter(line => line !== '' && !line.startsWith('#'));
263+
const patterns: string[] = [];
264+
const criticalPatterns = ['.context.md', '.context.yaml', '.context.json', '.contextdocs.md', '.contextignore'];
265+
266+
for (let i = 0; i < lines.length; i++) {
267+
const line = lines[i];
268+
269+
// Check for valid pattern format
270+
if (!/^[!#]?[\w\-./\*\?]+$/.test(line)) {
271+
console.error(` Error: Invalid ignore pattern on line ${i + 1}: ${line}`);
272+
}
273+
274+
// Check for redundant patterns
275+
if (patterns.includes(line)) {
276+
console.warn(` Warning: Redundant pattern on line ${i + 1}: ${line}`);
277+
}
278+
279+
// Check for critical file ignores
280+
for (const criticalPattern of criticalPatterns) {
281+
if (line.endsWith(criticalPattern) || line.includes(`/${criticalPattern}`)) {
282+
console.error(` Error: Ignoring critical context file on line ${i + 1}: ${line}`);
283+
}
284+
}
285+
286+
patterns.push(line);
287+
}
288+
289+
// Check for conflicting include/exclude patterns
290+
for (let i = 0; i < patterns.length; i++) {
291+
for (let j = i + 1; j < patterns.length; j++) {
292+
if (patterns[i].startsWith('!') && patterns[j] === patterns[i].slice(1) ||
293+
patterns[j].startsWith('!') && patterns[i] === patterns[j].slice(1)) {
294+
console.warn(` Warning: Conflicting patterns: "${patterns[i]}" and "${patterns[j]}"`);
295+
}
296+
}
297+
}
120298
}
121299
}
122300

0 commit comments

Comments
 (0)