eslint-plugin-ember-template-lint 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"