gscan 4.37.2 → 4.37.4

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.
@@ -16,6 +16,9 @@ function getNodeName(node) {
16
16
 
17
17
  case 'PathExpression':
18
18
  default:
19
+ if (node.data){
20
+ return `@${node.parts.join('.')}`;
21
+ }
19
22
  if (node.parts) {
20
23
  return node.parts[0];
21
24
  }
@@ -13,10 +13,10 @@ module.exports = {
13
13
  'GS090-NO-PRICE-DATA-CURRENCY-CONTEXT': require('./lint-no-price-data-currency-context'),
14
14
  'GS090-NO-PRICE-DATA-MONTHLY-YEARLY': require('./lint-no-price-data-monthly-yearly'),
15
15
  'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': require('./lint-no-unknown-page-properties'),
16
+ 'GS120-NO-UNKNOWN-GLOBALS': require('./lint-no-unknown-globals'),
16
17
  'no-multi-param-conditionals': require('./lint-no-multi-param-conditionals'),
17
18
  'no-nested-async-helpers': require('./lint-no-nested-async-helpers'),
18
19
  'no-prev-next-post-outside-post-context': require('./lint-no-prev-next-post-outside-post-context'),
19
- 'no-unknown-globals': require('./lint-no-unknown-globals'),
20
20
  'no-unknown-partials': require('./lint-no-unknown-partials'),
21
21
  'no-unknown-helpers': require('./lint-no-unknown-helpers')
22
22
  };
@@ -1,29 +1,36 @@
1
1
  const {getNodeName} = require('../../helpers');
2
2
  const _ = require('lodash');
3
3
 
4
- const globals = {
5
- site: {
6
- url: true,
7
- title: true,
8
- description: true,
9
- icon: true,
10
- logo: true,
11
- cover_image: true,
12
- twitter: true,
13
- facebook: true,
14
- navigation: true,
15
- timezone: true,
16
- lang: true
17
- },
18
- config: {
19
- posts_per_page: true
20
- },
21
- labs: {
22
- publicAPI: true,
23
- subscribers: true
24
- }
4
+ // TODO: the allowlist should include all properties of these top-level globals
5
+ const ghostGlobals = {
6
+ site: true,
7
+ member: true,
8
+ setting: true, // TODO: we should remove this but the Journal theme is using it atm
9
+ config: true,
10
+ labs: true,
11
+ custom: true,
12
+ page: true
13
+ };
14
+
15
+ // unless we update AST to check that we're within a foreach block, we need to allowlist all of these as they look the same as globals
16
+ const dataVars = {
17
+ index: true,
18
+ number: true,
19
+ key: true,
20
+ first: true,
21
+ last: true,
22
+ odd: true,
23
+ even: true,
24
+ rowStart: true,
25
+ rowEnd: true
25
26
  };
26
27
 
28
+ // unless we move from lodash to a glob match, we just need to handle all custom since we can't allowlist those
29
+ function isOnAllowlist(parts) {
30
+ const variable = parts && parts[0];
31
+ return ghostGlobals[variable] || dataVars[variable] || false;
32
+ }
33
+
27
34
  // true = property exists
28
35
  // 'context' = has context's shape
29
36
  // ['context] = array of context's shape
@@ -218,10 +225,18 @@ class Scope {
218
225
  return matchedFrame && matchedFrame.node;
219
226
  }
220
227
 
221
- isLocal(node) {
222
- // @foo MustacheStatements are referencing globals rather than locals
228
+ isKnownVariable(node) {
229
+ // @foo statements are referencing globals rather than locals
230
+ // and can be detected with the data: true attribute (???)
231
+
232
+ // they can be direct [Mustache] statements {{@foo}}...
223
233
  if (node.type === 'MustacheStatement' && node.path.data) {
224
- return _.get(globals, node.path.parts.join('.'));
234
+ return isOnAllowlist(node.path.parts);
235
+ }
236
+
237
+ // ... or indirect using helpers, e.g. {{#match @foo.bar}}{{/match}}
238
+ if (node.type === 'PathExpression') {
239
+ return isOnAllowlist(node.parts);
225
240
  }
226
241
 
227
242
  let name = getNodeName(node);
@@ -2,8 +2,8 @@ const Rule = require('./base');
2
2
  const {logNode} = require('../helpers');
3
3
 
4
4
  module.exports = class NoUnknownGlobals extends Rule {
5
- _checkForUnknownGlobal(node) {
6
- if (node.path.data && !this.scope.isLocal(node)) {
5
+ _checkMustacheForUnknownGlobal(node) {
6
+ if (node.path.data && !this.scope.isKnownVariable(node)) {
7
7
  this.log({
8
8
  message: `${logNode(node)} is not a known global`,
9
9
  line: node.loc && node.loc.start.line,
@@ -13,9 +13,25 @@ module.exports = class NoUnknownGlobals extends Rule {
13
13
  }
14
14
  }
15
15
 
16
+ _checkBlockForUnknownGlobal(node) {
17
+ if (node.path.type === 'PathExpression') {
18
+ node.params.forEach((param) => {
19
+ if (param.data && !this.scope.isKnownVariable(param)) {
20
+ this.log({
21
+ message: `${logNode(param)} is not a known global`,
22
+ line: param.loc && param.loc.start.line,
23
+ column: param.loc && param.loc.start.column,
24
+ source: this.sourceForNode(param)
25
+ });
26
+ }
27
+ });
28
+ }
29
+ }
30
+
16
31
  visitor() {
17
32
  return {
18
- MustacheStatement: this._checkForUnknownGlobal.bind(this)
33
+ MustacheStatement: this._checkMustacheForUnknownGlobal.bind(this),
34
+ BlockStatement: this._checkBlockForUnknownGlobal.bind(this)
19
35
  };
20
36
  }
21
37
  };
@@ -116,10 +116,7 @@ function parseWithAST({theme, log, file, rules, callback}) {
116
116
 
117
117
  const ruleImplementations = {
118
118
  'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
119
- isEnabled: ({options}) => {
120
- // TODO: change to `isEnabled: true` when removing labs flag
121
- return options && options.labs && options.labs.pageImprovements;
122
- },
119
+ isEnabled: true,
123
120
  init: ({result}) => {
124
121
  result.pageBuilderProperties = new Set();
125
122
  },
@@ -143,16 +140,14 @@ const ruleImplementations = {
143
140
 
144
141
  notUsedProperties.forEach((property) => {
145
142
  log.failure({
146
- ref: `@page.${property}`
143
+ ref: `page.hbs`,
144
+ message: `@page.${property} is not used`
147
145
  });
148
146
  });
149
147
  }
150
148
  },
151
149
  'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': {
152
- isEnabled: ({options}) => {
153
- // TODO: change to `isEnabled: true` when removing labs flag
154
- return options && options.labs && options.labs.pageImprovements;
155
- },
150
+ isEnabled: true,
156
151
  eachFile: ({file, theme, log}) => {
157
152
  const templateTest = file.file.match(/(?<!partials\/.+?)\.hbs$/);
158
153
 
@@ -0,0 +1,136 @@
1
+ const _ = require('lodash');
2
+ const spec = require('../specs');
3
+ const versions = require('../utils').versions;
4
+ const ASTLinter = require('../ast-linter');
5
+ const {normalizePath} = require('../utils');
6
+
7
+ function processFileFunction(files, failures, theme, partialsFound) {
8
+ const processedFiles = [];
9
+
10
+ return function processFile(linter, themeFile, parentInlinePartials = []) {
11
+ if (processedFiles.includes(themeFile.file)) {
12
+ return;
13
+ }
14
+
15
+ processedFiles.push(themeFile.file);
16
+
17
+ // Reset inline partial variables
18
+ linter.inlinePartials = [];
19
+ linter.options.inlinePartials = [];
20
+
21
+ linter.verify({
22
+ parsed: themeFile.parsed,
23
+ rules: [
24
+ require('../ast-linter/rules/mark-declared-inline-partials')
25
+ ],
26
+ source: themeFile.content,
27
+ moduleId: themeFile.file
28
+ });
29
+
30
+ // Store the inline partials for the actual partial linting
31
+ const inlinePartials = linter.inlinePartials;
32
+ linter.options.inlinePartials = [...inlinePartials, ...parentInlinePartials];
33
+
34
+ const astResults = linter.verify({
35
+ parsed: themeFile.parsed,
36
+ rules: [
37
+ require('../ast-linter/rules/mark-used-partials'),
38
+ require('../ast-linter/rules/lint-no-unknown-globals')
39
+ ],
40
+ source: themeFile.content,
41
+ moduleId: themeFile.file
42
+ });
43
+
44
+ if (astResults.length) {
45
+ astResults.forEach((result) => {
46
+ failures.push({
47
+ ref: themeFile.file,
48
+ message: result.message
49
+ });
50
+ });
51
+ }
52
+
53
+ theme.helpers = theme.helpers || {};
54
+ linter.helpers.forEach((helper) => {
55
+ if (!theme.helpers[helper.name]) {
56
+ theme.helpers[helper.name] = [];
57
+ }
58
+ theme.helpers[helper.name].push(themeFile.file);
59
+ });
60
+
61
+ linter.partials.forEach((partial) => {
62
+ const partialName = partial.node;
63
+ partialsFound[partialName] = true;
64
+ const file = files.find(f => normalizePath(f.file) === `partials/${normalizePath(partialName)}.hbs`);
65
+ if (file) {
66
+ // Find all inline partial declaration that were within the partial usage block
67
+ const childrenInlinePartials = [...parentInlinePartials];
68
+ for (const inline of inlinePartials) {
69
+ //Only partials that are in scope
70
+ if (inline.parents.some(node => node.type === partial.type &&
71
+ node.loc.source === partial.loc.source &&
72
+ node.loc.start.line === partial.loc.start.line &&
73
+ node.loc.start.column === partial.loc.start.column &&
74
+ node.loc.end.line === partial.loc.end.line &&
75
+ node.loc.end.column === partial.loc.end.column)) {
76
+ // Override the `parents` attribute as the inline partials are in another context than the children file
77
+ childrenInlinePartials.push({
78
+ ...inline,
79
+ parents: []
80
+ });
81
+ }
82
+ }
83
+ processFile(linter, file, childrenInlinePartials);
84
+ }
85
+ });
86
+ };
87
+ }
88
+
89
+ const checkNoUnknownGlobals = function checkNoUnknownGlobals(theme, options) {
90
+ const failures = [];
91
+ const checkVersion = _.get(options, 'checkVersion', versions.default);
92
+ const ruleSet = spec.get([checkVersion]);
93
+
94
+ let partialsFound = {};
95
+
96
+ // Reset theme.helpers to make sure we only get helpers that are used
97
+ theme.helpers = {};
98
+
99
+ // CASE: 001-deprecations checks only needs `rules` that start with `GS001-DEPR-`
100
+ const ruleRegex = /GS120-.*/g;
101
+
102
+ const rulesToCheck = _.pickBy(ruleSet.rules, function (rule, ruleCode) {
103
+ if (ruleCode.match(ruleRegex)) {
104
+ return rule;
105
+ }
106
+ });
107
+
108
+ const processFile = processFileFunction(theme.files, failures, theme, partialsFound);
109
+
110
+ _.each(rulesToCheck, function (check, ruleCode) {
111
+ const linter = new ASTLinter({
112
+ partials: theme.partials,
113
+ helpers: ruleSet.knownHelpers
114
+ });
115
+
116
+ _.each(theme.files, function (themeFile) {
117
+ let templateTest = themeFile.file.match(/(?<!partials\/.+?)\.hbs$/);
118
+
119
+ if (templateTest) {
120
+ processFile(linter, themeFile);
121
+ }
122
+ });
123
+
124
+ theme.partials = Object.keys(partialsFound);
125
+
126
+ if (failures.length > 0) {
127
+ theme.results.fail[ruleCode] = {failures: failures};
128
+ } else {
129
+ theme.results.pass.push(ruleCode);
130
+ }
131
+ });
132
+
133
+ return theme;
134
+ };
135
+
136
+ module.exports = checkNoUnknownGlobals;
@@ -713,20 +713,24 @@ let rules = {
713
713
  details: oneLineTrim`<code>config.custom</code> entry <code>description</code> should be less than <code>100</code> characters so that it is displayed correctly.<br />
714
714
  Check the <a href="${docsBaseUrl}custom-settings" target=_blank><code>config.custom</code> documentation</a> for further information.`
715
715
  },
716
-
717
716
  'GS110-NO-MISSING-PAGE-BUILDER-USAGE': {
718
717
  level: 'warning',
719
- rule: 'Not all page builder features are being used',
720
- // TODO: get proper docs link
721
- details: oneLineTrim`Some page builder features used via the <code>{{@page}}</code> global are not being used.&nbsp;
722
- Check the <a href="${docsBaseUrl}page-builder" target=_blank>page builder documentation</a> for further information.`
718
+ rule: 'Not all page features are being used',
719
+ details: oneLineTrim`Some page features used by Ghost via the <code>{{@page}}</code> global are not implemented in this theme.&nbsp;
720
+ Find more information about the <code>{{@page}}</code> global <a href="${docsBaseUrl}helpers/page/" target=_blank>here</a>.`
723
721
  },
724
-
725
722
  'GS110-NO-UNKNOWN-PAGE-BUILDER-USAGE': {
726
723
  level: 'error',
727
724
  fatal: true,
728
725
  rule: 'Unsupported page builder feature used',
729
- details: oneLineTrim`A page builder feature used via the <code>{{@page}}</code> global was detected but it's not supported by this version of Ghost.`
726
+ details: oneLineTrim`A page feature used via the <code>{{@page}}</code> global was detected but it's not supported by this version of Ghost.&nbsp;
727
+ Find more information about the <code>{{@page}}</code> global <a href="${docsBaseUrl}helpers/page/" target=_blank>here</a>.`
728
+ },
729
+ 'GS120-NO-UNKNOWN-GLOBALS': {
730
+ level: 'error',
731
+ rule: 'Unknown global helper used',
732
+ details: oneLineTrim`A global helper was detected that is not supported by this version of Ghost. Check the
733
+ <a href="${docsBaseUrl}helpers/" target=_blank>helpers documentation</a> for further information.`
730
734
  }
731
735
  };
732
736
 
package/lib/specs/v1.js CHANGED
@@ -387,7 +387,7 @@ rules = {
387
387
  level: 'error',
388
388
  rule: 'Templates must contain valid Handlebars',
389
389
  fatal: true,
390
- details: oneLineTrim`Oops! You seemed to have used invalid Handlebars syntax. This mostly happens, when you use a helper that is not supported.<br>
390
+ details: oneLineTrim`Oops! You seemed to have used invalid Handlebars syntax. This mostly happens when you use a helper that is not supported.<br>
391
391
  See the full list of available helpers <a href="${docsBaseUrl}helpers/" target=_blank>here</a>.`
392
392
  },
393
393
  'GS010-PJ-REQ': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gscan",
3
- "version": "4.37.2",
3
+ "version": "4.37.4",
4
4
  "description": "Scans Ghost themes looking for errors, deprecations, features and compatibility",
5
5
  "keywords": [
6
6
  "ghost",
@@ -40,7 +40,7 @@
40
40
  "gscan": "./bin/cli.js"
41
41
  },
42
42
  "dependencies": {
43
- "@sentry/node": "7.60.0",
43
+ "@sentry/node": "7.60.1",
44
44
  "@tryghost/config": "0.2.17",
45
45
  "@tryghost/debug": "0.1.25",
46
46
  "@tryghost/errors": "1.2.25",