@round-core/lint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -0
- package/src/index.js +7 -0
- package/src/offset-map.js +85 -0
- package/src/processor.js +65 -0
- package/src/state.js +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@round-core/lint",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint processor for RoundJS files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"eslint": "^8.0.0 || ^9.0.0"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@round-core/shared": "latest"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes line start offsets for a given source string.
|
|
3
|
+
* @param {string} source
|
|
4
|
+
* @returns {number[]} Array of offsets where each line starts.
|
|
5
|
+
*/
|
|
6
|
+
function computeLineStarts(source) {
|
|
7
|
+
const lineStarts = [0];
|
|
8
|
+
for (let i = 0; i < source.length; i++) {
|
|
9
|
+
if (source[i] === '\n') {
|
|
10
|
+
lineStarts.push(i + 1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return lineStarts;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts a 0-based offset to line and column (1-based).
|
|
18
|
+
* @param {number} offset
|
|
19
|
+
* @param {number[]} lineStarts
|
|
20
|
+
* @returns {{line: number, column: number}}
|
|
21
|
+
*/
|
|
22
|
+
function offsetToPos(offset, lineStarts) {
|
|
23
|
+
let low = 0;
|
|
24
|
+
let high = lineStarts.length - 1;
|
|
25
|
+
|
|
26
|
+
while (low <= high) {
|
|
27
|
+
const mid = (low + high) >>> 1;
|
|
28
|
+
if (lineStarts[mid] <= offset) {
|
|
29
|
+
low = mid + 1;
|
|
30
|
+
} else {
|
|
31
|
+
high = mid - 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lineIndex = high; // 0-based
|
|
36
|
+
const lineStart = lineStarts[lineIndex];
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
line: lineIndex + 1,
|
|
40
|
+
column: offset - lineStart + 1
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remaps ESLint messages to original source positions.
|
|
46
|
+
* @param {any[]} messages
|
|
47
|
+
* @param {{source: string, mapper: any}} state
|
|
48
|
+
* @returns {any[]}
|
|
49
|
+
*/
|
|
50
|
+
export function remapMessages(messages, state) {
|
|
51
|
+
const { source, mapper } = state;
|
|
52
|
+
const lineStarts = computeLineStarts(source);
|
|
53
|
+
|
|
54
|
+
return messages.map(msg => {
|
|
55
|
+
if (!msg.range && !msg.line) return msg;
|
|
56
|
+
|
|
57
|
+
let startOffset, endOffset;
|
|
58
|
+
|
|
59
|
+
if (msg.range) {
|
|
60
|
+
startOffset = msg.range[0];
|
|
61
|
+
endOffset = msg.range[1];
|
|
62
|
+
} else {
|
|
63
|
+
return msg;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remap offsets using SourceMapper
|
|
67
|
+
const originalStart = mapper.remap(startOffset);
|
|
68
|
+
const originalEnd = mapper.remap(endOffset);
|
|
69
|
+
|
|
70
|
+
// Calculate new line/column
|
|
71
|
+
const startPos = offsetToPos(originalStart, lineStarts);
|
|
72
|
+
const endPos = offsetToPos(originalEnd, lineStarts);
|
|
73
|
+
|
|
74
|
+
// Clone and update message
|
|
75
|
+
return {
|
|
76
|
+
...msg,
|
|
77
|
+
line: startPos.line,
|
|
78
|
+
column: startPos.column,
|
|
79
|
+
endLine: endPos.line,
|
|
80
|
+
endColumn: endPos.column,
|
|
81
|
+
range: [originalStart, originalEnd],
|
|
82
|
+
fix: undefined
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
package/src/processor.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SourceMapper, runPreprocess } from '@round-core/shared';
|
|
2
|
+
import { fileState } from './state.js';
|
|
3
|
+
import { remapMessages } from './offset-map.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
preprocess(code, filename) {
|
|
7
|
+
const mapper = new SourceMapper();
|
|
8
|
+
runPreprocess(code, mapper, 0);
|
|
9
|
+
|
|
10
|
+
// MAGIC: Suppress false positives for RoundJS syntax without react-plugin
|
|
11
|
+
|
|
12
|
+
// 1. Identify top-level names (imports and declarations)
|
|
13
|
+
const topLevelNames = new Set();
|
|
14
|
+
const declMatches = mapper.code.matchAll(/^\s*(?:export\s+)?(?:function|const|let|var|class|import)\s+({[\s\S]*?}|[a-zA-Z0-9_$]+)/gm);
|
|
15
|
+
for (const m of declMatches) {
|
|
16
|
+
const inner = m[1];
|
|
17
|
+
if (inner.startsWith('{')) {
|
|
18
|
+
const innerNames = inner.match(/([a-zA-Z0-9_$]+)/g);
|
|
19
|
+
if (innerNames) innerNames.forEach(n => topLevelNames.add(n));
|
|
20
|
+
} else {
|
|
21
|
+
topLevelNames.add(inner);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Mark components used in JSX as "used"
|
|
26
|
+
const cleanForComponents = mapper.code.replace(/`[\s\S]*?`/g, '');
|
|
27
|
+
const componentMatches = cleanForComponents.matchAll(/<([A-Z][a-zA-Z0-9_$]*)/g);
|
|
28
|
+
const components = new Set([...componentMatches].map(m => m[1]));
|
|
29
|
+
|
|
30
|
+
let magic = '\n\n/* eslint-disable */\n/* globals RoundControlFlow, e, item */\n;';
|
|
31
|
+
|
|
32
|
+
// Usage for top-level components only
|
|
33
|
+
for (const comp of components) {
|
|
34
|
+
if (topLevelNames.has(comp)) {
|
|
35
|
+
magic += `${comp};`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mapper.add(magic, code.length);
|
|
40
|
+
|
|
41
|
+
fileState.set(filename, {
|
|
42
|
+
source: code,
|
|
43
|
+
mapper: mapper
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return [mapper.code];
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
postprocess(messages, filename) {
|
|
50
|
+
const state = fileState.get(filename);
|
|
51
|
+
|
|
52
|
+
// Clean up state (optional, but good for memory if ESLint is long running)
|
|
53
|
+
// However, ESLint might run multiple fixes?
|
|
54
|
+
// For now we keep it simple. If we delete here, we might lose it if
|
|
55
|
+
// specific flows access it again. But usually postprocess is the end.
|
|
56
|
+
fileState.delete(filename);
|
|
57
|
+
|
|
58
|
+
if (!state) return messages[0];
|
|
59
|
+
|
|
60
|
+
// Remap messages from the first (and only) part
|
|
61
|
+
return remapMessages(messages[0], state);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
supportsAutofix: false
|
|
65
|
+
};
|