Skip to content

Commit 4e77988

Browse files
Updating indent check. Added two new checks.
Signed-off-by: Steve Springett <steve@springett.us>
1 parent 9be3484 commit 4e77988

File tree

5 files changed

+368
-143
lines changed

5 files changed

+368
-143
lines changed

tools/src/main/js/linter/checks/formatting-indent.check.js

Lines changed: 135 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* CycloneDX Schema Linter - Formatting and Indentation Check
33
*
44
* Validates that JSON schemas follow consistent formatting rules:
5-
* - Correct indentation based on nesting level
5+
* - Correct indentation based on nesting level (2 spaces per level)
6+
* - No tabs
67
* - No trailing whitespace
78
* - Consistent line endings
89
* - Final newline
@@ -31,7 +32,7 @@ class FormattingIndentCheck extends LintCheck {
3132
const spaces = config.spaces ?? 2;
3233
const requireFinalNewline = config.requireFinalNewline ?? true;
3334
const allowTrailingWhitespace = config.allowTrailingWhitespace ?? false;
34-
const lineEnding = config.lineEnding ?? 'lf'; // lf or crlf
35+
const lineEnding = config.lineEnding ?? 'lf';
3536

3637
const lines = rawContent.split(/\r?\n/);
3738

@@ -62,127 +63,156 @@ class FormattingIndentCheck extends LintCheck {
6263
));
6364
}
6465

65-
// Check trailing whitespace
66-
if (!allowTrailingWhitespace) {
67-
lines.forEach((line, index) => {
68-
if (/[ \t]+$/.test(line)) {
69-
issues.push(this.createIssue(
70-
`Line ${index + 1} has trailing whitespace.`,
71-
'$',
72-
{ line: index + 1 },
73-
Severity.WARNING
74-
));
75-
}
76-
});
77-
}
66+
// Track nesting depth
67+
let depth = 0;
7868

79-
// Check for tabs
80-
lines.forEach((line, index) => {
81-
if (/^\t/.test(line)) {
69+
for (let i = 0; i < lines.length; i++) {
70+
const line = lines[i];
71+
const lineNum = i + 1;
72+
const trimmed = line.trim();
73+
74+
// Skip empty lines
75+
if (trimmed === '') continue;
76+
77+
// Check trailing whitespace
78+
if (!allowTrailingWhitespace && /[ \t]+$/.test(line)) {
8279
issues.push(this.createIssue(
83-
`Line ${index + 1} uses tabs for indentation. Use ${spaces} spaces instead.`,
80+
`Line ${lineNum} has trailing whitespace.`,
8481
'$',
85-
{ line: index + 1 }
82+
{ line: lineNum },
83+
Severity.WARNING
8684
));
8785
}
88-
});
89-
90-
// Generate canonical formatting and compare line by line
91-
try {
92-
const canonical = JSON.stringify(schema, null, spaces);
93-
const canonicalLines = canonical.split('\n');
9486

95-
// Normalise input lines (remove \r)
96-
const normalisedLines = rawContent.replace(/\r\n/g, '\n').split('\n');
87+
// Check for tabs in indentation
88+
if (/^\t+/.test(line)) {
89+
issues.push(this.createIssue(
90+
`Line ${lineNum} uses tabs for indentation. Use ${spaces} spaces instead.`,
91+
'$',
92+
{ line: lineNum }
93+
));
94+
// Still update depth tracking
95+
depth += this.getDepthChange(trimmed);
96+
continue;
97+
}
98+
99+
// Count openers/closers outside of strings for this line
100+
const { openersBeforeContent, closersAtStart, netChange } = this.analyzeLineStructure(trimmed);
97101

98-
// Remove trailing empty line from normalised if it exists (final newline)
99-
if (normalisedLines[normalisedLines.length - 1] === '') {
100-
normalisedLines.pop();
102+
// If line starts with closer, decrease depth for this line
103+
if (closersAtStart > 0) {
104+
depth = Math.max(0, depth - closersAtStart);
101105
}
102106

103-
// Compare line by line
104-
const maxLines = Math.max(canonicalLines.length, normalisedLines.length);
107+
// Validate indentation
108+
const actualIndent = this.getLeadingSpaces(line);
109+
const expectedIndent = depth * spaces;
105110

106-
for (let i = 0; i < maxLines; i++) {
107-
const expected = canonicalLines[i];
108-
const actual = normalisedLines[i];
109-
110-
// Handle missing lines
111-
if (expected === undefined) {
112-
issues.push(this.createIssue(
113-
`Line ${i + 1} is unexpected. File has more lines than expected.`,
114-
'$',
115-
{ line: i + 1, actual: this.truncate(actual, 50) }
116-
));
117-
continue;
118-
}
119-
120-
if (actual === undefined) {
121-
issues.push(this.createIssue(
122-
`Line ${i + 1} is missing. Expected: "${this.truncate(expected, 50)}"`,
123-
'$',
124-
{ line: i + 1, expected: this.truncate(expected, 50) }
125-
));
126-
continue;
127-
}
128-
129-
// Compare indentation
130-
const expectedIndent = this.getLeadingSpaces(expected);
131-
const actualIndent = this.getLeadingSpaces(actual);
132-
133-
if (expectedIndent !== actualIndent) {
134-
issues.push(this.createIssue(
135-
`Line ${i + 1} has incorrect indentation. Expected ${expectedIndent} spaces, found ${actualIndent}.`,
136-
'$',
137-
{
138-
line: i + 1,
139-
expectedIndent,
140-
actualIndent,
141-
content: this.truncate(actual.trim(), 40)
142-
}
143-
));
144-
continue;
145-
}
146-
147-
// Compare content (after trimming to focus on structure)
148-
const expectedTrimmed = expected.trim();
149-
const actualTrimmed = actual.trim();
150-
151-
if (expectedTrimmed !== actualTrimmed) {
152-
// Check if it's a key ordering difference
153-
if (this.isKeyOrderDifference(expectedTrimmed, actualTrimmed)) {
154-
issues.push(this.createIssue(
155-
`Line ${i + 1} has different key ordering than canonical format.`,
156-
'$',
157-
{
158-
line: i + 1,
159-
expected: this.truncate(expectedTrimmed, 50),
160-
actual: this.truncate(actualTrimmed, 50)
161-
},
162-
Severity.INFO
163-
));
164-
} else {
165-
issues.push(this.createIssue(
166-
`Line ${i + 1} content differs from canonical format.`,
167-
'$',
168-
{
169-
line: i + 1,
170-
expected: this.truncate(expectedTrimmed, 50),
171-
actual: this.truncate(actualTrimmed, 50)
172-
},
173-
Severity.WARNING
174-
));
111+
if (actualIndent !== expectedIndent) {
112+
issues.push(this.createIssue(
113+
`Line ${lineNum} has incorrect indentation. Expected ${expectedIndent} spaces, found ${actualIndent}.`,
114+
'$',
115+
{
116+
line: lineNum,
117+
expectedIndent,
118+
actualIndent,
119+
content: this.truncate(trimmed, 40)
175120
}
176-
}
121+
));
177122
}
178123

179-
} catch (err) {
180-
// JSON parsing errors are handled elsewhere
124+
// Update depth for next line based on net openers (excluding leading closers already handled)
125+
depth += netChange + closersAtStart; // Add back closersAtStart since netChange includes them
126+
depth = Math.max(0, depth);
181127
}
182128

183129
return issues;
184130
}
185131

132+
/**
133+
* Analyze line structure for bracket counting
134+
* Returns closers at start, and net depth change
135+
*/
136+
analyzeLineStructure(trimmed) {
137+
let closersAtStart = 0;
138+
let inString = false;
139+
let escapeNext = false;
140+
let openers = 0;
141+
let closers = 0;
142+
let foundNonBracket = false;
143+
144+
for (let i = 0; i < trimmed.length; i++) {
145+
const char = trimmed[i];
146+
147+
if (escapeNext) {
148+
escapeNext = false;
149+
continue;
150+
}
151+
152+
if (char === '\\' && inString) {
153+
escapeNext = true;
154+
continue;
155+
}
156+
157+
if (char === '"') {
158+
inString = !inString;
159+
foundNonBracket = true;
160+
continue;
161+
}
162+
163+
if (inString) continue;
164+
165+
if (char === '{' || char === '[') {
166+
openers++;
167+
foundNonBracket = true;
168+
} else if (char === '}' || char === ']') {
169+
closers++;
170+
// Count closers at start (before any non-bracket content)
171+
if (!foundNonBracket) {
172+
closersAtStart++;
173+
}
174+
} else if (!/\s/.test(char)) {
175+
foundNonBracket = true;
176+
}
177+
}
178+
179+
return {
180+
openersBeforeContent: openers,
181+
closersAtStart,
182+
netChange: openers - closers
183+
};
184+
}
185+
186+
/**
187+
* Get simple depth change (openers - closers) for a line
188+
*/
189+
getDepthChange(trimmed) {
190+
let inString = false;
191+
let escapeNext = false;
192+
let change = 0;
193+
194+
for (const char of trimmed) {
195+
if (escapeNext) {
196+
escapeNext = false;
197+
continue;
198+
}
199+
if (char === '\\' && inString) {
200+
escapeNext = true;
201+
continue;
202+
}
203+
if (char === '"') {
204+
inString = !inString;
205+
continue;
206+
}
207+
if (inString) continue;
208+
209+
if (char === '{' || char === '[') change++;
210+
else if (char === '}' || char === ']') change--;
211+
}
212+
213+
return change;
214+
}
215+
186216
/**
187217
* Get the number of leading spaces in a string
188218
*/
@@ -198,15 +228,6 @@ class FormattingIndentCheck extends LintCheck {
198228
if (str.length <= maxLength) return str;
199229
return str.substring(0, maxLength - 3) + '...';
200230
}
201-
202-
/**
203-
* Check if difference is likely due to key ordering
204-
*/
205-
isKeyOrderDifference(expected, actual) {
206-
// Both are object keys
207-
const keyPattern = /^"[^"]+"\s*:/;
208-
return keyPattern.test(expected) && keyPattern.test(actual);
209-
}
210231
}
211232

212233
// Create and register the check

tools/src/main/js/linter/checks/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export async function loadAllChecks() {
3838
export * from './schema-id-pattern.check.js';
3939
export * from './schema-comment.check.js';
4040
export * from './schema-draft.check.js';
41+
export * from './model-property-order.check.js';
42+
export * from './model-structure.check.js';
4143
export * from './formatting-indent.check.js';
4244
export * from './description-full-stop.check.js';
4345
export * from './meta-enum-full-stop.check.js';

0 commit comments

Comments
 (0)