eslint-plugin-ember-template-lint 0.2.0 → 0.6.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.
@@ -12,6 +12,7 @@ const allMessages = {};
12
12
  const reporter = {
13
13
  setup(context) {
14
14
  this.getSourceCode = context.getSourceCode;
15
+ this.getPhysicalFilename = context.getPhysicalFilename;
15
16
  },
16
17
  report(message) {
17
18
  message.meta = {
@@ -42,14 +43,8 @@ class Rule {
42
43
  enter(node) {
43
44
  if (!activeRules.get(node)) {
44
45
  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
46
  reporter.setup(context);
52
- runTemplateLint(node.contents, filename, reporter, scopeVars, start);
47
+ runTemplateLint(node, reporter);
53
48
  }
54
49
  activeRules.set(node, activeRules.get(node) + 1);
55
50
  },
@@ -108,4 +103,4 @@ module.exports = {
108
103
  configs: configs,
109
104
  rules: createRules(rules),
110
105
  configuredRules: configuredRules
111
- };
106
+ };
@@ -1,34 +1,12 @@
1
- const { preprocess, traverse, Source } = require('@glimmer/syntax');
2
1
  const gts = require('ember-template-imports');
3
2
  const typescriptParser = require('@typescript-eslint/parser');
4
3
  const typescriptEstree = require('@typescript-eslint/typescript-estree');
5
4
 
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
5
 
27
6
  function replaceRange(s, start, end, substitute) {
28
7
  return s.substring(0, start) + substitute + s.substring(end);
29
8
  }
30
9
 
31
-
32
10
  module.exports = {
33
11
  parseForESLint(code, options) {
34
12
  let jsCode = code;
@@ -43,14 +21,15 @@ module.exports = {
43
21
  });
44
22
  const emptyText = '[`' + lines.join('\n').slice(4) + '`]';
45
23
  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;
24
+ const ast = {
25
+ type: '__TEMPLATE__Template',
26
+ };
27
+ ast.range = range;
28
+ ast.contents = template;
50
29
  tpl.ast = ast;
51
30
  });
52
31
  const result = typescriptParser.parseForESLint(jsCode, options);
53
- const visitorKeys = {...result.visitorKeys};
32
+ const visitorKeys = {...result.visitorKeys, '__TEMPLATE__Template': []};
54
33
  result.ast.tokens.forEach((token) => {
55
34
  if (token.type === 'Template') {
56
35
  const range = [token.range[0] - 1, token.range[1] + 1];
@@ -58,8 +37,6 @@ module.exports = {
58
37
  if (!template) return;
59
38
  const ast = template.ast;
60
39
  ast.loc = token.loc;
61
- const source = new Source(code);
62
- Object.assign(visitorKeys, visitorKeysForAst(source, ast));
63
40
  Object.assign(token, ast);
64
41
  }
65
42
  });
@@ -1,4 +1,3 @@
1
- const { preprocess, traverse, Source } = require('@glimmer/syntax');
2
1
 
3
2
  class Scope {
4
3
  type = 'global';
@@ -26,25 +25,22 @@ class ScopeManager {
26
25
 
27
26
  module.exports = {
28
27
  parseForESLint(code) {
29
- const source = new Source(code);
30
- const ast = preprocess(source, { mode: 'codemod' });
31
- const tokens = [];
32
28
  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);
29
+ const types = new Set(['Program']);
30
+ const ast = {};
31
+ ast.tokens = [ast];
32
+ ast.range = [0, code.length];
33
+ const lines = code.split('\n');
34
+ ast.loc = {
35
+ start: {
36
+ line: 1,
37
+ column: 1
38
+ },
39
+ end: {
40
+ line: lines.length,
41
+ column: lines[lines.length - 1].length
45
42
  }
46
- });
47
- ast.tokens = tokens;
43
+ };
48
44
  ast.comments = comments;
49
45
  const visitorKeys = {};
50
46
  types.forEach((t) => {
@@ -53,9 +49,6 @@ module.exports = {
53
49
  ast.type = 'Program';
54
50
  ast.isHbs = true;
55
51
  ast.contents = code;
56
- tokens.slice(1).forEach((node) => {
57
- node.type = `__TEMPLATE__${node.type}`;
58
- });
59
52
  const scope = new ScopeManager();
60
53
  scope.globalScope.block = ast;
61
54
  return { ast, scopeManager: scope, visitorKeys };
@@ -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.length];
17
+ d.range = [d.offset, d.offset + (d.deleteText?.length || 0)];
18
18
  });
19
19
  lintMessages.forEach((m) => {
20
20
  m.range = [
@@ -1,21 +1,77 @@
1
1
  const { runAsWorker } = require('synckit');
2
2
  const { generateDifferences } = require('prettier-linter-helpers');
3
3
 
4
- runAsWorker(async (filename, text, options) => {
4
+ async function _applyFixes(options, results, columnOffset) {
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
+ const spaces = ' '.repeat(columnOffset);
18
+
19
+ for (let ruleName of ruleNames) {
20
+ let templateInfos = [{
21
+ template: currentSource,
22
+ columnOffset: 0,
23
+ isStrictMode: false
24
+ }];
25
+
26
+ for (let templateInfo of templateInfos) {
27
+
28
+ let rule = this._buildRule(ruleName, {
29
+ shouldFix: true,
30
+ filePath: options.filePath,
31
+ columnOffset: templateInfo.columnOffset,
32
+ rawSource: templateInfo.template,
33
+ isStrictMode: templateInfo.isStrictMode,
34
+ fileConfig,
35
+ });
36
+
37
+ let visitor = await rule.getVisitor();
38
+ let { code } = transform(templateInfo.template, () => visitor);
39
+
40
+ if (code !== templateInfo.template) {
41
+ let template = templateInfo.template;
42
+ if (columnOffset) {
43
+ template = spaces + template.split('\n').join('\n' + spaces);
44
+ }
45
+ if (columnOffset) {
46
+ code = spaces + code.split('\n').join('\n' + spaces);
47
+ }
48
+ const diffs = generateDifferences(template, code);
49
+ fixableIssues.filter(r => r.rule === ruleName).forEach((r, i) => {
50
+ r.fix = diffs[i];
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ return currentSource;
57
+ }
58
+
59
+ runAsWorker(async (filename, text, options, columnOffset=0) => {
5
60
  const Lint = await import('ember-template-lint');
6
61
  const lint = new Lint.default(options);
7
62
  process.env.emberTemplateLintFileName = filename;
8
63
  process.env.emberTemplateLintFixMode = false;
64
+
9
65
  const messages = await lint.verify({
10
- source: text
66
+ source: text,
67
+ filePath: filename.replace('.gts', '.hbs')
11
68
  });
12
69
  process.env.emberTemplateLintFixMode = true;
13
- const fixedText = (await lint.verifyAndFix({
14
- source: text
15
- })).output;
16
- const diff = generateDifferences(text, fixedText);
70
+ await _applyFixes.call(lint,{
71
+ source: text,
72
+ filePath: filename.replace('.gts', '.hbs'),
73
+ }, messages, columnOffset);
17
74
  return {
18
75
  messages,
19
- diff
20
76
  };
21
77
  });
package/lib/rules/lint.js CHANGED
@@ -3,53 +3,58 @@ const synckit = require('synckit');
3
3
  const DocumentLines = require('../utils/document');
4
4
  const { templateLintConfig } = require('../ember-teplate-lint/config');
5
5
 
6
- function runTemplateLint(text, filename, context, scopeVars=[], offset=0) {
6
+ function runTemplateLint(node, context) {
7
+ const sourceCode = context.getSourceCode();
8
+ const { scopeManager, text: sourceCodeText } = sourceCode;
9
+ const scopes = scopeManager.scopes.filter(x => x.block.range[0] < node.range[0] && x.block.range[1] > node.range[1]);
10
+ const scopeVars = scopes.map(s => s.variables.map(x => x.name)).flat();
11
+ const filename = context.getPhysicalFilename();
12
+ let text = node.contents;
13
+ const initialLine = sourceCodeText.split('\n')[node.loc.start.line-1];
14
+ const columnOffset = initialLine.match(/\s+/)?.[0].length || 0;
15
+ const offset = (node.range[0] || 0);
16
+ const spaces = ' '.repeat(columnOffset);
17
+ const originalDocument = new DocumentLines(spaces + text);
18
+ if (columnOffset) {
19
+ text = text.split('\n').map(l => l.replace(new RegExp(`^\\s{1,${columnOffset}}`), '')).join('\n');
20
+ text = text.trim();
21
+ }
22
+
7
23
  try {
8
24
  const syncFn = synckit.createSyncFn(require.resolve('./hbs-worker'));
9
- const response = syncFn(filename, text, { config: templateLintConfig });
25
+ const response = syncFn(filename, text, { config: templateLintConfig }, columnOffset);
10
26
  const lintMessages = response.messages;
11
- const diffs = response.diff;
12
- const document = new DocumentLines(text);
13
- diffs.forEach((d) => {
14
- d.range = [d.offset, d.offset + d.deleteText.length];
15
- });
16
27
  lintMessages.forEach((m) => {
28
+ if (m.fix) {
29
+ const d = m.fix;
30
+ m.fix.range = [d.offset, d.offset + (d.deleteText?.length || 0)];
31
+ m.range = m.fix.range;
32
+ const start = originalDocument.offsetToPosition(m.fix.range[0]);
33
+ const end = originalDocument.offsetToPosition(m.fix.range[1]);
34
+ m.fix.range = [
35
+ originalDocument.positionToOffset({
36
+ line: start.line,
37
+ character: start.character
38
+ }),
39
+ originalDocument.positionToOffset({
40
+ line: end.line,
41
+ character: end.character
42
+ })
43
+ ];
44
+ m.fix.range = m.fix.range.map(x => offset + x);
45
+ }
17
46
  m.range = [
18
- document.positionToOffset({
47
+ originalDocument.positionToOffset({
19
48
  line: m.line - 1,
20
- character: m.column - 1
49
+ character: m.column
21
50
  }),
22
- document.positionToOffset({
51
+ originalDocument.positionToOffset({
23
52
  line: m.endLine - 1,
24
- character: m.endColumn - 1
53
+ character: m.endColumn
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ember-template-lint",
3
- "version": "0.2.0",
3
+ "version": "0.6.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
- "synckit": "^0.8.5"
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"