eslint-plugin-ember-template-lint 0.1.0 → 0.5.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/README.md +20 -0
- package/lib/config/base.js +20 -0
- package/lib/config/index.js +20 -0
- package/lib/ember-teplate-lint/config.js +18 -0
- package/lib/ember-teplate-lint/info.js +111 -0
- package/lib/ember-teplate-lint/worker.js +12 -0
- package/lib/parser/gts-parser.js +80 -0
- package/lib/parser/hbs-parser.js +63 -0
- package/lib/processor.js +11 -0
- package/lib/rules/config.js +1 -1
- package/lib/rules/hbs-worker.js +53 -6
- package/lib/rules/lint.js +40 -35
- package/lib/utils/document.js +100 -0
- package/package.json +6 -5
package/README.md
CHANGED
@@ -24,14 +24,34 @@ Add `hbs` to the plugins section of your `.eslintrc` configuration file. You can
|
|
24
24
|
|
25
25
|
### 2. Modify your `.eslintrc.js`
|
26
26
|
|
27
|
+
you can use the presets from here https://github.com/ember-template-lint/ember-template-lint#presets
|
28
|
+
like this:
|
29
|
+
`plugin:ember-template-lint/<preset>`
|
30
|
+
|
31
|
+
to just use the `.template-lintrc.js` config file just use
|
32
|
+
`plugin:ember-template-lint/config`
|
33
|
+
|
34
|
+
to load ember-template-lint plugins in eslinrc you need to manually register them:
|
35
|
+
```js
|
36
|
+
require('eslint-plugin-ember-template-lint/lib/ember-teplate-lint/config').registerPlugin('ember-template-lint-plugin-prettier');
|
37
|
+
```
|
38
|
+
|
39
|
+
## IMPORTANT
|
40
|
+
Do NOT set parser to anything in your .eslintrc.js as it will override the parser from here
|
41
|
+
|
42
|
+
for typescript parser you can extend its base config, then it will work correctly
|
43
|
+
|
27
44
|
```js
|
28
45
|
// .eslintrc.js
|
29
46
|
// optional:
|
30
47
|
require('eslint-plugin-ember-template-lint/lib/ember-teplate-lint/config').registerPlugin('ember-template-lint-plugin-prettier');
|
31
48
|
|
49
|
+
|
32
50
|
module.exports = {
|
33
51
|
extends: [
|
34
52
|
'eslint:recommended',
|
53
|
+
'plugin:@typescript-eslint/base',
|
54
|
+
'plugin:ember-template-lint/config',
|
35
55
|
'plugin:ember-template-lint/recommended',
|
36
56
|
//optional:
|
37
57
|
'plugin:ember-template-lint/ember-template-lint-plugin-prettier:recommended'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module.exports = {
|
2
|
+
root: true,
|
3
|
+
|
4
|
+
plugins: ['ember-template-lint'],
|
5
|
+
|
6
|
+
rules: {},
|
7
|
+
|
8
|
+
overrides: [
|
9
|
+
{
|
10
|
+
files: ['**/*.hbs'],
|
11
|
+
parser: require.resolve('../parser/hbs-parser'),
|
12
|
+
processor: 'ember-template-lint/noop'
|
13
|
+
},
|
14
|
+
{
|
15
|
+
files: ['**/*.gts', '**/*.gjs'],
|
16
|
+
parser: require.resolve('../parser/gts-parser'),
|
17
|
+
processor: 'ember-template-lint/noop'
|
18
|
+
},
|
19
|
+
],
|
20
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
const info = require('../ember-teplate-lint/info');
|
2
|
+
const base = require('./base');
|
3
|
+
|
4
|
+
const configs = {};
|
5
|
+
|
6
|
+
Object.entries(info.configs).forEach(([name, config]) => {
|
7
|
+
Object.entries(config.rules).forEach(([name, conf]) => {
|
8
|
+
if (typeof conf == 'boolean') {
|
9
|
+
config.rules[name] = [conf ? 'error' : 'off'];
|
10
|
+
}
|
11
|
+
});
|
12
|
+
configs[name] = {...base, rules: config.rules};
|
13
|
+
});
|
14
|
+
configs['config'] = {
|
15
|
+
...base,
|
16
|
+
rules: info.configuredRules
|
17
|
+
};
|
18
|
+
|
19
|
+
module.exports = configs;
|
20
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
const templateLintConfig = {
|
2
|
+
rules: {},
|
3
|
+
plugins: [],
|
4
|
+
};
|
5
|
+
|
6
|
+
function registerRule(name, config) {
|
7
|
+
templateLintConfig.rules[name] = config;
|
8
|
+
}
|
9
|
+
|
10
|
+
function registerPlugin(name) {
|
11
|
+
templateLintConfig.plugins.push(name);
|
12
|
+
}
|
13
|
+
|
14
|
+
module.exports = {
|
15
|
+
registerRule,
|
16
|
+
registerPlugin,
|
17
|
+
templateLintConfig
|
18
|
+
};
|
@@ -0,0 +1,111 @@
|
|
1
|
+
const synckit = require('synckit');
|
2
|
+
const syncFn = synckit.createSyncFn(require.resolve('./worker'));
|
3
|
+
const { runTemplateLint } = require('../rules/lint');
|
4
|
+
const { registerRule, templateLintConfig } = require('../ember-teplate-lint/config');
|
5
|
+
|
6
|
+
const lintWithEslintConfigs = syncFn({ config: templateLintConfig });
|
7
|
+
const lintConfigs = syncFn();
|
8
|
+
|
9
|
+
const activeRules = new Map();
|
10
|
+
const allMessages = {};
|
11
|
+
|
12
|
+
const reporter = {
|
13
|
+
setup(context) {
|
14
|
+
this.getSourceCode = context.getSourceCode;
|
15
|
+
},
|
16
|
+
report(message) {
|
17
|
+
message.meta = {
|
18
|
+
fixable: 'code'
|
19
|
+
};
|
20
|
+
allMessages[message.rule] = allMessages[message.rule] || [];
|
21
|
+
allMessages[message.rule].push(message);
|
22
|
+
}
|
23
|
+
};
|
24
|
+
|
25
|
+
class Rule {
|
26
|
+
|
27
|
+
constructor(name) {
|
28
|
+
this.create = this.create.bind(this);
|
29
|
+
this.name = name;
|
30
|
+
this.meta = {
|
31
|
+
fixable: 'code'
|
32
|
+
};
|
33
|
+
}
|
34
|
+
create(context) {
|
35
|
+
const rule = this;
|
36
|
+
let options = context.options;
|
37
|
+
if (options.length === 0) {
|
38
|
+
options = true;
|
39
|
+
}
|
40
|
+
registerRule(this.name, options);
|
41
|
+
const visitor = {
|
42
|
+
enter(node) {
|
43
|
+
if (!activeRules.get(node)) {
|
44
|
+
activeRules.set(node, 0);
|
45
|
+
const sourceCode = context.getSourceCode();
|
46
|
+
const { scopeManager } = sourceCode;
|
47
|
+
const scopes = scopeManager.scopes.filter(x => x.block.range[0] < node.range[0] && x.block.range[1] > node.range[1]);
|
48
|
+
const scopeVars = scopes.map(s => s.variables.map(x => x.name)).flat();
|
49
|
+
const filename = context.getPhysicalFilename();
|
50
|
+
const start = node.range[0] || 0;
|
51
|
+
reporter.setup(context);
|
52
|
+
runTemplateLint(node.contents, filename, reporter, scopeVars, start);
|
53
|
+
}
|
54
|
+
activeRules.set(node, activeRules.get(node) + 1);
|
55
|
+
},
|
56
|
+
exit(node) {
|
57
|
+
const messages = allMessages[rule.name] || [];
|
58
|
+
messages.forEach(m => context.report(m));
|
59
|
+
activeRules.set(node, activeRules.get(node) - 1);
|
60
|
+
allMessages[rule.name] = [];
|
61
|
+
},
|
62
|
+
};
|
63
|
+
return {
|
64
|
+
'Program': (node) => node.isHbs && visitor.enter(node),
|
65
|
+
'Program:exit': (node) => node.isHbs && visitor.exit(node),
|
66
|
+
'__TEMPLATE__Template': visitor.enter,
|
67
|
+
'__TEMPLATE__Template:exit': visitor.exit,
|
68
|
+
};
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
function createRules(rules) {
|
73
|
+
const created = rules.map(r => new Rule(r));
|
74
|
+
const map = {};
|
75
|
+
created.forEach(r => {
|
76
|
+
map[r.name] = r;
|
77
|
+
});
|
78
|
+
return map;
|
79
|
+
}
|
80
|
+
|
81
|
+
const configs = {
|
82
|
+
...lintWithEslintConfigs.configs,
|
83
|
+
...lintConfigs.configs
|
84
|
+
};
|
85
|
+
|
86
|
+
const rules = [...new Set([...lintConfigs.rules, ...lintWithEslintConfigs.rules])];
|
87
|
+
|
88
|
+
delete configs.recommended.overrides;
|
89
|
+
|
90
|
+
Object.values(configs).forEach((config) => {
|
91
|
+
const rules = {};
|
92
|
+
Object.entries(config.rules).forEach(([rule, conf]) => {
|
93
|
+
rules['ember-template-lint/' + rule] = conf;
|
94
|
+
});
|
95
|
+
config.rules = rules;
|
96
|
+
});
|
97
|
+
|
98
|
+
|
99
|
+
const configuredRules = {};
|
100
|
+
Object.entries(lintConfigs.configuredRules).forEach(([name, conf]) => {
|
101
|
+
configuredRules['ember-template-lint/' + name] = conf.config && [conf.severity];
|
102
|
+
if (typeof conf.config !== 'boolean') {
|
103
|
+
configuredRules['ember-template-lint/' + name].push(conf.config);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
|
107
|
+
module.exports = {
|
108
|
+
configs: configs,
|
109
|
+
rules: createRules(rules),
|
110
|
+
configuredRules: configuredRules
|
111
|
+
};
|
@@ -0,0 +1,12 @@
|
|
1
|
+
const { runAsWorker } = require('synckit');
|
2
|
+
|
3
|
+
runAsWorker(async (options) => {
|
4
|
+
const Lint = await import('ember-template-lint');
|
5
|
+
const lint = new Lint.default(options);
|
6
|
+
await lint.loadConfig();
|
7
|
+
return {
|
8
|
+
configs: lint.config.loadedConfigurations,
|
9
|
+
rules: Object.keys(lint.config.loadedRules),
|
10
|
+
configuredRules: lint.config.rules,
|
11
|
+
};
|
12
|
+
});
|
@@ -0,0 +1,80 @@
|
|
1
|
+
const { preprocess, traverse, Source } = require('@glimmer/syntax');
|
2
|
+
const gts = require('ember-template-imports');
|
3
|
+
const typescriptParser = require('@typescript-eslint/parser');
|
4
|
+
const typescriptEstree = require('@typescript-eslint/typescript-estree');
|
5
|
+
|
6
|
+
function visitorKeysForAst(source, ast) {
|
7
|
+
const tokens = [];
|
8
|
+
const types = new Set();
|
9
|
+
traverse(ast, {
|
10
|
+
All(node) {
|
11
|
+
types.add(`__TEMPLATE__${node.type}`);
|
12
|
+
tokens.push(node);
|
13
|
+
}
|
14
|
+
});
|
15
|
+
ast.tokens = tokens;
|
16
|
+
const visitorKeys = {};
|
17
|
+
types.forEach((t) => {
|
18
|
+
// visitorKeys[t] = ['body', 'name', 'path', 'params', 'attributes', 'hash', 'modifiers', 'comments', 'value', 'program', 'inverse', 'children']
|
19
|
+
visitorKeys[t] = [];
|
20
|
+
});
|
21
|
+
tokens.forEach((node) => {
|
22
|
+
node.type = `__TEMPLATE__${node.type}`;
|
23
|
+
});
|
24
|
+
return visitorKeys;
|
25
|
+
}
|
26
|
+
|
27
|
+
function replaceRange(s, start, end, substitute) {
|
28
|
+
return s.substring(0, start) + substitute + s.substring(end);
|
29
|
+
}
|
30
|
+
|
31
|
+
|
32
|
+
module.exports = {
|
33
|
+
parseForESLint(code, options) {
|
34
|
+
let jsCode = code;
|
35
|
+
const templateInfos = gts.parseTemplates(jsCode, options.filePath);
|
36
|
+
templateInfos.forEach((tpl) => {
|
37
|
+
const range = [tpl.start.index, tpl.end.index + tpl.end[0].length];
|
38
|
+
tpl.range = range;
|
39
|
+
const template = jsCode.slice(...range);
|
40
|
+
const lines = template.split('\n');
|
41
|
+
lines.forEach((line, i) => {
|
42
|
+
lines[i] = ' '.repeat(line.length);
|
43
|
+
});
|
44
|
+
const emptyText = '[`' + lines.join('\n').slice(4) + '`]';
|
45
|
+
jsCode = replaceRange(jsCode, ...range, emptyText);
|
46
|
+
const source = new Source(tpl.contents);
|
47
|
+
const ast = preprocess(source, { mode: 'codemod' });
|
48
|
+
ast.range = [tpl.start.index + tpl.start[0].length + 1, tpl.end.index];
|
49
|
+
ast.contents = tpl.contents;
|
50
|
+
tpl.ast = ast;
|
51
|
+
});
|
52
|
+
const result = typescriptParser.parseForESLint(jsCode, options);
|
53
|
+
const visitorKeys = {...result.visitorKeys};
|
54
|
+
result.ast.tokens.forEach((token) => {
|
55
|
+
if (token.type === 'Template') {
|
56
|
+
const range = [token.range[0] - 1, token.range[1] + 1];
|
57
|
+
const template = templateInfos.find(t => t.range[0] >= range[0] && t.range[1] <= range[1]);
|
58
|
+
if (!template) return;
|
59
|
+
const ast = template.ast;
|
60
|
+
ast.loc = token.loc;
|
61
|
+
const source = new Source(code);
|
62
|
+
Object.assign(visitorKeys, visitorKeysForAst(source, ast));
|
63
|
+
Object.assign(token, ast);
|
64
|
+
}
|
65
|
+
});
|
66
|
+
typescriptEstree.simpleTraverse(result.ast, {
|
67
|
+
enter(node) {
|
68
|
+
if (node.type === 'TemplateLiteral') {
|
69
|
+
const range = [node.range[0] - 1, node.range[1] + 1];
|
70
|
+
const template = templateInfos.find(t => t.range[0] >= range[0] && t.range[1] <= range[1]);
|
71
|
+
if (!template) return;
|
72
|
+
const ast = template.ast;
|
73
|
+
Object.assign(node, ast);
|
74
|
+
}
|
75
|
+
}
|
76
|
+
});
|
77
|
+
|
78
|
+
return { ...result, visitorKeys };
|
79
|
+
}
|
80
|
+
};
|
@@ -0,0 +1,63 @@
|
|
1
|
+
const { preprocess, traverse, Source } = require('@glimmer/syntax');
|
2
|
+
|
3
|
+
class Scope {
|
4
|
+
type = 'global';
|
5
|
+
variables = [];
|
6
|
+
through= [];
|
7
|
+
set = new Map();
|
8
|
+
upper = null;
|
9
|
+
childScopes = [];
|
10
|
+
references = [];
|
11
|
+
block = null;
|
12
|
+
}
|
13
|
+
|
14
|
+
class ScopeManager {
|
15
|
+
globalScope = new Scope();
|
16
|
+
scopes = [this.globalScope];
|
17
|
+
|
18
|
+
acquire() {
|
19
|
+
return;
|
20
|
+
}
|
21
|
+
|
22
|
+
getDeclaredVariables() {
|
23
|
+
return [];
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
module.exports = {
|
28
|
+
parseForESLint(code) {
|
29
|
+
const source = new Source(code);
|
30
|
+
const ast = preprocess(source, { mode: 'codemod' });
|
31
|
+
const tokens = [];
|
32
|
+
const comments = [];
|
33
|
+
const types = new Set();
|
34
|
+
types.add('Program');
|
35
|
+
traverse(ast, {
|
36
|
+
All(node) {
|
37
|
+
types.add(`__TEMPLATE__${node.type}`);
|
38
|
+
const span = source.spanFor(node.loc);
|
39
|
+
node.range = [span.getStart().offset, span.getEnd().offset];
|
40
|
+
if (node.type.toLowerCase().includes('comment')) {
|
41
|
+
comments.push(node);
|
42
|
+
return;
|
43
|
+
}
|
44
|
+
tokens.push(node);
|
45
|
+
}
|
46
|
+
});
|
47
|
+
ast.tokens = tokens;
|
48
|
+
ast.comments = comments;
|
49
|
+
const visitorKeys = {};
|
50
|
+
types.forEach((t) => {
|
51
|
+
visitorKeys[t] = [];
|
52
|
+
});
|
53
|
+
ast.type = 'Program';
|
54
|
+
ast.isHbs = true;
|
55
|
+
ast.contents = code;
|
56
|
+
tokens.slice(1).forEach((node) => {
|
57
|
+
node.type = `__TEMPLATE__${node.type}`;
|
58
|
+
});
|
59
|
+
const scope = new ScopeManager();
|
60
|
+
scope.globalScope.block = ast;
|
61
|
+
return { ast, scopeManager: scope, visitorKeys };
|
62
|
+
}
|
63
|
+
};
|
package/lib/processor.js
ADDED
package/lib/rules/config.js
CHANGED
@@ -14,7 +14,7 @@ const runTemplateLint = (text, filename, context, scopeVars=[], offset=0, option
|
|
14
14
|
const diffs = response.diff;
|
15
15
|
const document = new DocumentLines(text);
|
16
16
|
diffs.forEach((d) => {
|
17
|
-
d.range = [d.offset, d.offset + d.deleteText
|
17
|
+
d.range = [d.offset, d.offset + (d.deleteText?.length || 0)];
|
18
18
|
});
|
19
19
|
lintMessages.forEach((m) => {
|
20
20
|
m.range = [
|
package/lib/rules/hbs-worker.js
CHANGED
@@ -1,21 +1,68 @@
|
|
1
1
|
const { runAsWorker } = require('synckit');
|
2
2
|
const { generateDifferences } = require('prettier-linter-helpers');
|
3
3
|
|
4
|
+
async function _applyFixes(options, results) {
|
5
|
+
const { transform } = await import('ember-template-recast');
|
6
|
+
let currentSource = options.source;
|
7
|
+
let fixableIssues = results.filter((r) => r.isFixable);
|
8
|
+
|
9
|
+
// nothing to do, bail out
|
10
|
+
if (fixableIssues.length === 0) {
|
11
|
+
return currentSource;
|
12
|
+
}
|
13
|
+
|
14
|
+
let fileConfig = this._moduleStatusCache.getConfigForFile(options);
|
15
|
+
|
16
|
+
let ruleNames = new Set(fixableIssues.map((r) => r.rule));
|
17
|
+
|
18
|
+
for (let ruleName of ruleNames) {
|
19
|
+
let templateInfos = [{
|
20
|
+
template: currentSource,
|
21
|
+
columnOffset: 0,
|
22
|
+
isStrictMode: false
|
23
|
+
}];
|
24
|
+
|
25
|
+
for (let templateInfo of templateInfos) {
|
26
|
+
|
27
|
+
let rule = this._buildRule(ruleName, {
|
28
|
+
shouldFix: true,
|
29
|
+
filePath: options.filePath,
|
30
|
+
columnOffset: templateInfo.columnOffset,
|
31
|
+
rawSource: templateInfo.template,
|
32
|
+
isStrictMode: templateInfo.isStrictMode,
|
33
|
+
fileConfig,
|
34
|
+
});
|
35
|
+
|
36
|
+
let visitor = await rule.getVisitor();
|
37
|
+
let { code } = transform(templateInfo.template, () => visitor);
|
38
|
+
|
39
|
+
if (code !== templateInfo.template) {
|
40
|
+
const diffs = generateDifferences(templateInfo.template, code);
|
41
|
+
fixableIssues.filter(r => r.rule === ruleName).forEach((r, i) => {
|
42
|
+
r.fix = diffs[i];
|
43
|
+
});
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
return currentSource;
|
49
|
+
}
|
50
|
+
|
4
51
|
runAsWorker(async (filename, text, options) => {
|
5
52
|
const Lint = await import('ember-template-lint');
|
6
53
|
const lint = new Lint.default(options);
|
7
54
|
process.env.emberTemplateLintFileName = filename;
|
8
55
|
process.env.emberTemplateLintFixMode = false;
|
9
56
|
const messages = await lint.verify({
|
10
|
-
source: text
|
57
|
+
source: text,
|
58
|
+
filePath: filename.replace('.gts', '.hbs')
|
11
59
|
});
|
12
60
|
process.env.emberTemplateLintFixMode = true;
|
13
|
-
|
14
|
-
source: text
|
15
|
-
|
16
|
-
|
61
|
+
await _applyFixes.call(lint,{
|
62
|
+
source: text,
|
63
|
+
filePath: filename.replace('.gts', '.hbs')
|
64
|
+
}, messages);
|
17
65
|
return {
|
18
66
|
messages,
|
19
|
-
diff
|
20
67
|
};
|
21
68
|
});
|
package/lib/rules/lint.js
CHANGED
@@ -4,52 +4,57 @@ const DocumentLines = require('../utils/document');
|
|
4
4
|
const { templateLintConfig } = require('../ember-teplate-lint/config');
|
5
5
|
|
6
6
|
function runTemplateLint(text, filename, context, scopeVars=[], offset=0) {
|
7
|
+
const originalDocument = new DocumentLines(text);
|
7
8
|
try {
|
9
|
+
let columnOffset = 0;
|
10
|
+
let lineOffset = 0;
|
11
|
+
if (text.startsWith('\n')) {
|
12
|
+
text = text.slice(1);
|
13
|
+
lineOffset += 1;
|
14
|
+
}
|
15
|
+
columnOffset = text.match(/\s+/)[0].length;
|
16
|
+
text = text.split('\n').map(l => l.replace(new RegExp(`^\\s{1,${columnOffset}}`), '')).join('\n');
|
17
|
+
text = text.trim();
|
8
18
|
const syncFn = synckit.createSyncFn(require.resolve('./hbs-worker'));
|
9
19
|
const response = syncFn(filename, text, { config: templateLintConfig });
|
10
20
|
const lintMessages = response.messages;
|
11
|
-
const diffs = response.diff;
|
12
21
|
const document = new DocumentLines(text);
|
13
|
-
diffs.forEach((d) => {
|
14
|
-
d.range = [d.offset, d.offset + d.deleteText.length];
|
15
|
-
});
|
16
22
|
lintMessages.forEach((m) => {
|
23
|
+
if (m.fix) {
|
24
|
+
const d = m.fix;
|
25
|
+
if (d.insertText) {
|
26
|
+
d.insertText = d.insertText.replace(/\n/g, '\n' + ' '.repeat(columnOffset));
|
27
|
+
}
|
28
|
+
m.fix.range = [d.offset, d.offset + (d.deleteText?.length || 0)];
|
29
|
+
m.range = m.fix.range;
|
30
|
+
const start = document.offsetToPosition(m.fix.range[0]);
|
31
|
+
const end = document.offsetToPosition(m.fix.range[1]);
|
32
|
+
m.fix.range = [
|
33
|
+
originalDocument.positionToOffset({
|
34
|
+
line: start.line + lineOffset,
|
35
|
+
character: start.character + columnOffset - 1
|
36
|
+
}),
|
37
|
+
originalDocument.positionToOffset({
|
38
|
+
line: end.line + lineOffset,
|
39
|
+
character: end.character + columnOffset - 1
|
40
|
+
})
|
41
|
+
];
|
42
|
+
m.fix.range = m.fix.range.map(x => offset + x);
|
43
|
+
m.range = m.fix.range;
|
44
|
+
return;
|
45
|
+
}
|
17
46
|
m.range = [
|
18
|
-
|
19
|
-
line: m.line - 1,
|
20
|
-
character: m.column - 1
|
47
|
+
originalDocument.positionToOffset({
|
48
|
+
line: m.line - 1 + lineOffset,
|
49
|
+
character: m.column + columnOffset - 1
|
21
50
|
}),
|
22
|
-
|
23
|
-
line: m.endLine - 1,
|
24
|
-
character: m.endColumn - 1
|
51
|
+
originalDocument.positionToOffset({
|
52
|
+
line: m.endLine - 1 + lineOffset,
|
53
|
+
character: m.endColumn + columnOffset - 1
|
25
54
|
})];
|
26
|
-
const isInside = (d) => d.range[0] >= m.range[0] && d.range[1] <= m.range[1];
|
27
|
-
const doesContain = (d) => d.range[0] <= m.range[0] && d.range[1] >= m.range[1];
|
28
|
-
const idx = diffs.findIndex(d => isInside(d) || doesContain(d));
|
29
|
-
if (idx !== -1) {
|
30
|
-
const d = diffs.splice(idx, 1);
|
31
|
-
m.fix = d[0];
|
32
|
-
m.fix.range = m.fix.range.map(x => offset + x);
|
33
|
-
}
|
34
55
|
m.range = m.range.map(x => offset + x);
|
35
56
|
});
|
36
57
|
|
37
|
-
if (diffs.length) {
|
38
|
-
diffs.forEach((d) => {
|
39
|
-
const range = d.range;
|
40
|
-
const [start, end] = range.map(index =>
|
41
|
-
context.getSourceCode().getLocFromIndex(index)
|
42
|
-
);
|
43
|
-
context.report({
|
44
|
-
fix: (fixer) => {
|
45
|
-
return fixer.replaceTextRange(range, d.fix.insertText || '');
|
46
|
-
},
|
47
|
-
loc: { start, end },
|
48
|
-
message: 'template error',
|
49
|
-
});
|
50
|
-
});
|
51
|
-
}
|
52
|
-
|
53
58
|
lintMessages.forEach((msg) => {
|
54
59
|
if (msg.rule === 'no-implicit-this') {
|
55
60
|
if (scopeVars.includes(msg.source)) {
|
@@ -80,4 +85,4 @@ function runTemplateLint(text, filename, context, scopeVars=[], offset=0) {
|
|
80
85
|
|
81
86
|
module.exports = {
|
82
87
|
runTemplateLint
|
83
|
-
};
|
88
|
+
};
|
@@ -0,0 +1,100 @@
|
|
1
|
+
/**
|
2
|
+
* @typedef {{ line: number; character: number }} Position
|
3
|
+
*/
|
4
|
+
|
5
|
+
// Helper class to convert line/column from and to offset
|
6
|
+
// taken and adapt from https://github.com/typed-ember/glint/blob/main/packages/core/src/language-server/util/position.ts
|
7
|
+
class DocumentLines {
|
8
|
+
/**
|
9
|
+
* @param {string} contents
|
10
|
+
*/
|
11
|
+
constructor(contents) {
|
12
|
+
this.lineStarts = computeLineStarts(contents);
|
13
|
+
}
|
14
|
+
|
15
|
+
/**
|
16
|
+
* @param {Position} position
|
17
|
+
* @return {number}
|
18
|
+
*/
|
19
|
+
positionToOffset(position) {
|
20
|
+
const { line, character } = position;
|
21
|
+
return this.lineStarts[line] + character;
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
*
|
26
|
+
* @param {number} position
|
27
|
+
* @return {Position}
|
28
|
+
*/
|
29
|
+
offsetToPosition(position) {
|
30
|
+
const lineStarts = this.lineStarts;
|
31
|
+
let line = 0;
|
32
|
+
while (line + 1 < lineStarts.length && lineStarts[line + 1] <= position) {
|
33
|
+
line++;
|
34
|
+
}
|
35
|
+
const character = position - lineStarts[line];
|
36
|
+
return { line, character };
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* @returns {number[]}
|
42
|
+
* @param {string} text
|
43
|
+
*/
|
44
|
+
function computeLineStarts(text) {
|
45
|
+
const result = [];
|
46
|
+
let pos = 0;
|
47
|
+
let lineStart = 0;
|
48
|
+
while (pos < text.length) {
|
49
|
+
const ch = text.codePointAt(pos);
|
50
|
+
pos++;
|
51
|
+
switch (ch) {
|
52
|
+
case 13 /* carriageReturn */: {
|
53
|
+
if (text.codePointAt(pos) === 10 /* lineFeed */) {
|
54
|
+
pos++;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
// falls through
|
58
|
+
case 10 /* lineFeed */: {
|
59
|
+
result.push(lineStart);
|
60
|
+
lineStart = pos;
|
61
|
+
break;
|
62
|
+
}
|
63
|
+
default: {
|
64
|
+
if (ch > 127 /* maxAsciiCharacter */ && isLineBreak(ch)) {
|
65
|
+
result.push(lineStart);
|
66
|
+
lineStart = pos;
|
67
|
+
}
|
68
|
+
break;
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
result.push(lineStart);
|
73
|
+
return result;
|
74
|
+
}
|
75
|
+
|
76
|
+
/* istanbul ignore next */
|
77
|
+
/**
|
78
|
+
* @param {number} ch
|
79
|
+
* @return {boolean}
|
80
|
+
*/
|
81
|
+
function isLineBreak(ch) {
|
82
|
+
// ES5 7.3:
|
83
|
+
// The ECMAScript line terminator characters are listed in Table 3.
|
84
|
+
// Table 3: Line Terminator Characters
|
85
|
+
// Code Unit Value Name Formal Name
|
86
|
+
// \u000A Line Feed <LF>
|
87
|
+
// \u000D Carriage Return <CR>
|
88
|
+
// \u2028 Line separator <LS>
|
89
|
+
// \u2029 Paragraph separator <PS>
|
90
|
+
// Only the characters in Table 3 are treated as line terminators. Other new line or line
|
91
|
+
// breaking characters are treated as white space but not as line terminators.
|
92
|
+
return (
|
93
|
+
ch === 10 /* lineFeed */ ||
|
94
|
+
ch === 13 /* carriageReturn */ ||
|
95
|
+
ch === 8232 /* lineSeparator */ ||
|
96
|
+
ch === 8233 /* paragraphSeparator */
|
97
|
+
);
|
98
|
+
}
|
99
|
+
|
100
|
+
module.exports = DocumentLines;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "eslint-plugin-ember-template-lint",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.5.0",
|
4
4
|
"description": "Provide linting for ember template",
|
5
5
|
"keywords": [
|
6
6
|
"eslint",
|
@@ -17,13 +17,14 @@
|
|
17
17
|
},
|
18
18
|
"dependencies": {
|
19
19
|
"@glimmer/syntax": "^0.84.3",
|
20
|
-
"typescript": "^5.0.4",
|
21
20
|
"@typescript-eslint/parser": "^5.59.7",
|
22
21
|
"@typescript-eslint/typescript-estree": "^5.59.7",
|
23
22
|
"ember-template-imports": "^3.4.2",
|
24
|
-
"prettier-linter-helpers": "^1.0.0",
|
25
23
|
"ember-template-lint": "^5.7.3",
|
26
|
-
"
|
24
|
+
"ember-template-recast": "^6.1.4",
|
25
|
+
"prettier-linter-helpers": "^1.0.0",
|
26
|
+
"synckit": "^0.8.5",
|
27
|
+
"typescript": "^5.0.4"
|
27
28
|
},
|
28
29
|
"peerDependencies": {
|
29
30
|
"ember-template-lint": "^5.7.3"
|
@@ -45,6 +46,6 @@
|
|
45
46
|
},
|
46
47
|
"license": "ISC",
|
47
48
|
"files": [
|
48
|
-
"lib
|
49
|
+
"lib/**/*.js"
|
49
50
|
]
|
50
51
|
}
|