ilib-lint 2.7.2 → 2.8.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 CHANGED
@@ -20,9 +20,12 @@ This i18n linter differs from other static linters in the following ways:
20
20
  a rule that checks that the translations in a resource file of a plural resource
21
21
  contain the correct set of plural categories for the target language.
22
22
  * It can load plugins
23
- * Parsers - you can add parsers for new programming languages or resource file types
24
- * Formatters - you can make the output look exactly the way you want
25
- * Rules - you can add new rules declaratively or programmatically
23
+ * Parsers - add parsers for new programming languages or resource file types
24
+ * Formatters - make the output look exactly the way you want
25
+ * Rules - add new rules declaratively or programmatically
26
+ * Fixers - apply auto-fixes to the file
27
+ * Transformers - transform the file in some way
28
+ * Serializers - serialize the file to a particular format
26
29
 
27
30
  See the [release notes](./docs/ReleaseNotes.md) for details on what is
28
31
  new and what has changed.
@@ -132,6 +135,17 @@ ilib-lint accepts the following command-line parameters:
132
135
  of 2. There is no default minimum, so the linter will not give an exit code
133
136
  unless this parameter is specified or unless one of the other limits is
134
137
  exceeded.
138
+ * write - if a file is modified in memory during the linter run, write
139
+ the modified file contents back out to the file system. If the file was not
140
+ modified, it will not be written out. The file name of the file will be calculated
141
+ using the template property of the matching file type in the configuration.
142
+ * autoFix - apply any auto-fixes that go along with the rules that were applied to
143
+ each file. If a file is successfully fixed, it can be written out. The autoFix
144
+ flag implies the write flag.
145
+ * overwrite - If the file is modified by a Fixer or a Transformer and written
146
+ out using a Serializer, it will be written back to the original file, overwriting
147
+ it. The template property of the file type will be ignored. This option implies
148
+ the write flag.
135
149
 
136
150
  If multiple limits are exceeded (maximum number of errors, warnings, or suggestions,
137
151
  or minimum I18N score), the exit code will be the most severe amongst them
@@ -230,12 +244,15 @@ plugins, which allow the plugin to define multiple plugins. For example, many
230
244
  plugins define multiple related rules at the same time which check for
231
245
  different aspects of a string.
232
246
 
233
- The linter plugin should override and implement these three methods:
247
+ The linter plugin should override and implement these methods:
234
248
 
235
249
  - getParsers - return an array of classes that inherit from the Parser class
236
250
  - getRules - return an array of classes that inherit from the Rule class
237
251
  - getRuleSets - return an array of named rule sets that define which rules to use
252
+ - getFixers - return an array of classes that inherit from the Fixer class
238
253
  - getFormatters - return an array of classes that inherit from the Formatter class
254
+ - getTransformers - return an array of classes that inherit from the Transformer class
255
+ - getSerializers - return an array of classes that inherit from the Serializer class
239
256
 
240
257
  For rules and formatters, each array entry can be either declarative or
241
258
  programmatic. See the descriptions below about declarative and programmatic
@@ -570,7 +587,7 @@ ilib-lint plugins from v1 of ilib-lint to v2.
570
587
 
571
588
  ## License
572
589
 
573
- Copyright © 2022-2024, JEDLSoft
590
+ Copyright © 2022-2025, JEDLSoft
574
591
 
575
592
  Licensed under the Apache License, Version 2.0 (the "License");
576
593
  you may not use this file except in compliance with the License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilib-lint",
3
- "version": "2.7.2",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tsconfig/node14": "^14.1.2",
53
+ "@types/jest": "^29.5.14",
53
54
  "@types/node": "^14.0.0",
54
55
  "docdash": "^2.0.2",
55
56
  "i18nlint-plugin-test-old": "file:test/i18nlint-plugin-test-old",
@@ -62,7 +63,6 @@
62
63
  },
63
64
  "dependencies": {
64
65
  "@formatjs/intl": "^2.10.4",
65
- "ilib-locale": "^1.2.2",
66
66
  "ilib-localeinfo": "^1.1.0",
67
67
  "intl-messageformat": "^10.5",
68
68
  "json5": "^2.2.3",
@@ -70,11 +70,13 @@
70
70
  "micromatch": "^4.0.7",
71
71
  "options-parser": "^0.4.0",
72
72
  "xml-js": "^1.6.11",
73
+ "ilib-lint-common": "^3.2.0",
73
74
  "ilib-common": "^1.1.6",
74
- "ilib-tools-common": "^1.12.2",
75
- "ilib-lint-common": "^3.1.2"
75
+ "ilib-locale": "^1.2.4",
76
+ "ilib-tools-common": "^1.14.0"
76
77
  },
77
78
  "scripts": {
79
+ "coverage": "pnpm test -- --coverage",
78
80
  "test": "pnpm test:jest",
79
81
  "test:jest": "LANG=en_US.UTF8 node --trace-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
80
82
  "test:watch": "pnpm test:jest --watch",
package/src/FileType.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * FileType.js - Represents a type of file in an ilib-lint project
3
3
  *
4
- * Copyright © 2023-2024 JEDLSoft
4
+ * Copyright © 2023-2025 JEDLSoft
5
5
  *
6
6
  * Licensed under the Apache License, Version 2.0 (the "License");
7
7
  * you may not use this file except in compliance with the License.
@@ -33,33 +33,79 @@ const logger = log4js.getLogger("ilib-lint.FileType");
33
33
  */
34
34
  class FileType {
35
35
  /**
36
- * Contructor a new instance of a file type. The options
37
- * for a file type must contain the following properties:
38
- *
39
- * - name - the name or glob spec for this file type
40
- * - project - the Project that this file type is a part of
41
- *
42
- * Additionally, the options may optionally contain the
43
- * following properties:
44
- *
45
- * - locales (Array of String) - list of locales to use with this file type,
46
- * which overrides the global locales for the project
47
- * - type (String) - specifies the way that files of this file type
48
- * are parsed. This can be one of "resource", "source", "line", or
49
- * "ast".
50
- * - template (String) - the path name template for this file type
51
- * which shows how to extract the locale from the path
52
- * name if the path includes it. Many file types do not
53
- * include the locale in the path, and in those cases,
54
- * the template can be left out.
55
- * - ruleset (Array of String) - a list of rule set names
56
- * to use with this file type
57
- * - parsers (Array of String) - an array of names of parsers to
58
- * apply to this file type. This is mainly useful when the source
59
- * code is in a file with an unexpected or ambiguous file
60
- * name extension. For example, a ".js" file may contain
61
- * regular Javascript code, but it may also be React JSX
62
- * code, or even Javascript with JSX and Flow type definitions.
36
+ * The lint project that this file is a part of.
37
+ * @type {Project}
38
+ */
39
+ project;
40
+
41
+ /**
42
+ * The name or glob spec for this file type
43
+ * @type {String|undefined}
44
+ */
45
+ name;
46
+
47
+ /**
48
+ * The list of locales to use with this file type
49
+ * @type {Array.<String>|undefined}
50
+ */
51
+ locales;
52
+
53
+ /**
54
+ * The intermediate representation type of this file type.
55
+ * @type {String}
56
+ */
57
+ type;
58
+
59
+ /**
60
+ * The array of names of classes of parsers to use with this file type.
61
+ * @type {Array.<String>|undefined}
62
+ */
63
+ parsers = undefined;
64
+
65
+ /**
66
+ * The array of classes of parsers to use with this file type.
67
+ * @type {Array.<Class>|undefined}
68
+ */
69
+ parserClasses = undefined;
70
+
71
+ /**
72
+ * The array of names of transformers to use with this file type.
73
+ * @type {Array.<String>|undefined}
74
+ */
75
+ transformers = undefined;
76
+
77
+ /**
78
+ * The array of instances of transformers to use with this file type.
79
+ * @type {Array.<Transformer>|undefined}
80
+ */
81
+ transformerInstances = undefined;
82
+
83
+ /**
84
+ * The serializer to use with this file type.
85
+ * @type {String|undefined}
86
+ */
87
+ serializer = undefined;
88
+
89
+ /**
90
+ * The instance of the serializer to use with this file type.
91
+ * @type {Serializer|undefined}
92
+ */
93
+ serializerInst = undefined;
94
+
95
+ /**
96
+ * The array of rule sets to apply to files of this type.
97
+ * @type {Array<String>|undefined}
98
+ */
99
+ ruleset = undefined;
100
+
101
+ /**
102
+ * The path template for this file type.
103
+ * @type {String|undefined}
104
+ */
105
+ template = undefined;
106
+
107
+ /**
108
+ * Contructor a new instance of a file type.
63
109
  *
64
110
  * The array of parsers will be used to attempt to parse each
65
111
  * source file. If a parser throws an exception/error while parsing,
@@ -70,19 +116,59 @@ class FileType {
70
116
  *
71
117
  * @param {Object} options the options governing the construction
72
118
  * of this file type as documented above
119
+ * @param {String} options.name the name or glob spec for this file type
120
+ * @param {Project} options.project the Project that this file type is a part of
121
+ * @param {Array.<String>} [options.locales] list of locales to use with this file type
122
+ * @param {String} [options.template] the path name template for this file type
123
+ * which shows how to extract the locale from the path
124
+ * name if the path includes it. Many file types
125
+ * do not include the locale in the path, and in those cases,
126
+ * the template can be left out. If a serializer is specified, then the template
127
+ * is also used by the serializer to determine how to name the output files.
128
+ * @param {Array.<String>} [options.parsers] an array of names of parsers to
129
+ * apply to this file type. The first parser is the most important one, as its
130
+ * type will be used to determine the type of intermediate representation, the
131
+ * type of the rules, and the type of the transformers and serializer. If no
132
+ * parsers are specified, then the parser manager will be asked to find all
133
+ * parsers that can parse files of this type.
134
+ * @param {Array.<String>} [options.ruleset] a list of rule set names
135
+ * to use with this file type. Only rules in these rule sets that operate
136
+ * on the same type of intermediate representation as the parsers will
137
+ * be applied to the file.
138
+ * @param {Array.<String>} [options.transformers] an array of transformer names
139
+ * to apply to files of this type after the rules have been applied. Every transformer
140
+ * must operate on the same type of intermediate representation as the parser.
141
+ * @param {String|Object} [options.serializer] the name of the serializer to use if
142
+ * the file has been modified by a transformer or a fixer. The serializer must
143
+ * operate on the same type of intermediate representation as the parser.
73
144
  * @constructor
145
+ * @throws {Error} if a transformer or serializer is specified that does not
146
+ * operate on the same type of intermediate representation as the parser, or
147
+ * if a parser, transformer, or serializer cannot be found.
74
148
  */
75
149
  constructor(options) {
76
150
  if (!options || !options.name || !options.project) {
77
151
  throw "Missing required options to the FileType constructor";
78
152
  }
79
- ["name", "project", "locales", "ruleset", "template", "type", "parsers"].forEach(prop => {
153
+ ["name", "project", "locales", "ruleset", "template", "parsers", "transformers", "serializer"].forEach(prop => {
80
154
  if (typeof(options[prop]) !== 'undefined') {
81
155
  this[prop] = options[prop];
82
156
  }
83
157
  });
84
158
 
85
- this.type = this.type || "string";
159
+ if (this.parsers) {
160
+ const parserMgr = this.project.getParserManager();
161
+ this.parserClasses = this.parsers.map(parserName => {
162
+ const parser = parserMgr.getByName(parserName);
163
+ if (!parser) {
164
+ throw `Could not find parser ${parserName} named in the configuration for filetype ${this.name}`;
165
+ }
166
+ if (!this.type) {
167
+ this.type = parserMgr.getType(parserName);
168
+ }
169
+ return parser;
170
+ });
171
+ }
86
172
 
87
173
  if (this.ruleset) {
88
174
  if (typeof(this.ruleset) === 'string') {
@@ -100,16 +186,44 @@ class FileType {
100
186
  }
101
187
  }
102
188
 
103
- if (this.parsers) {
104
- const parserMgr = this.project.getParserManager();
105
- this.parserClasses = this.parsers.map(parserName => {
106
- const parser = parserMgr.getByName(parserName);
107
- if (!parser) {
108
- throw `Could not find parser ${parserName} named in the configuration for filetype ${this.name}`;
189
+ if (this.transformers) {
190
+ const names = Array.isArray(this.transformers) ? this.transformers : [ this.transformers ];
191
+ const transformerMgr = this.project.getTransformerManager();
192
+ this.transformerInstances = names.map(transformerName => {
193
+ const transformer = transformerMgr.get(transformerName);
194
+ if (!transformer) {
195
+ throw `Could not find transformer ${transformerName} named in the configuration for filetype ${this.name}`;
109
196
  }
110
- return parser;
197
+ const transformerType = transformer.getType();
198
+ if (!this.type) {
199
+ this.type = transformerType;
200
+ } else if (transformerType !== this.type) {
201
+ throw new Error(`The transformer ${transformerName} processes representations of type ${transformerType}, but the filetype ${this.name} handles representations of type ${this.type}. The two types must match.`);
202
+ }
203
+ return transformer;
111
204
  });
112
205
  }
206
+
207
+ if (this.serializer) {
208
+ // if it is a string, then that string is the name of the serializer. If it is an object,
209
+ // then it the name and the settings to pass to the the serializer constructor.
210
+ const name = typeof(this.serializer) === 'string' ? this.serializer : this.serializer.name;
211
+ const serializerMgr = this.project.getSerializerManager();
212
+ this.serializerInst = serializerMgr.get(name, this.serializer);
213
+ if (!this.serializerInst) {
214
+ throw new Error(`Could not find or instantiate serializer ${this.serializer} named in the configuration for filetype ${this.name}`);
215
+ }
216
+ const serializerType = this.serializerInst.getType();
217
+ if (!this.type) {
218
+ this.type = serializerType;
219
+ } else if (serializerType !== this.type) {
220
+ throw new Error(`The serializer ${name} processes representations of type ${serializerType}, but the filetype ${this.name} handles representations of type ${this.type}. The two types must match.`);
221
+ }
222
+ }
223
+
224
+ if (!this.type) {
225
+ this.type = "string";
226
+ }
113
227
  }
114
228
 
115
229
  getName() {
@@ -195,6 +309,26 @@ class FileType {
195
309
  this.rules = set.getRules();
196
310
  return this.rules;
197
311
  }
312
+
313
+ /**
314
+ * Return the list of transformers to use with this file type.
315
+ *
316
+ * @returns {Array.<Transformer>|undefined} an array of transformer instances to use
317
+ * with this file type, or undefined if there are none.
318
+ */
319
+ getTransformers() {
320
+ return this.transformerInstances;
321
+ }
322
+
323
+ /**
324
+ * Return an instance of the serializer class for this file type.
325
+ *
326
+ * @returns {Serializer|undefined} an instance of the serializer class for this
327
+ * file type or undefined if there is no serializer for this file type.
328
+ */
329
+ getSerializer() {
330
+ return this.serializerInst;
331
+ }
198
332
  }
199
333
 
200
- export default FileType;
334
+ export default FileType;
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * LintableFile.js - Represent a lintable source file
3
3
  *
4
- * Copyright © 2022-2024 JEDLSoft
4
+ * Copyright © 2022-2025 JEDLSoft
5
5
  *
6
6
  * Licensed under the Apache License, Version 2.0 (the "License");
7
7
  * you may not use this file except in compliance with the License.
@@ -31,6 +31,36 @@ const logger = log4js.getLogger("ilib-lint.root.LintableFile");
31
31
  * @class Represent a source file
32
32
  */
33
33
  class LintableFile extends DirItem {
34
+ /**
35
+ * Whether the file has been modified since it was last written or since it was read.
36
+ * @type {boolean}
37
+ */
38
+ dirty = false;
39
+
40
+ /**
41
+ * The list of parsers that can parse this file.
42
+ * @type {Parser[]}
43
+ */
44
+ parsers = [];
45
+
46
+ /**
47
+ * The serializer for this file type.
48
+ * @type {Serializer|undefined}
49
+ */
50
+ serializer;
51
+
52
+ /**
53
+ * The file type of this source file
54
+ * @type {FileType}
55
+ */
56
+ filetype;
57
+
58
+ /**
59
+ * The intermediate representations of this file
60
+ * @type {IntermediateRepresentation[]}
61
+ */
62
+ irs = [];
63
+
34
64
  /**
35
65
  * Construct a source file instance
36
66
  * The options parameter can contain any of the following properties:
@@ -66,6 +96,8 @@ class LintableFile extends DirItem {
66
96
  extension = extension.substring(1);
67
97
  this.parsers = this.filetype.getParserClasses(extension);
68
98
  }
99
+ this.transformers = this.filetype.getTransformers();
100
+ this.serializer = this.filetype.getSerializer();
69
101
  }
70
102
 
71
103
  /**
@@ -169,7 +201,7 @@ class LintableFile extends DirItem {
169
201
  // and that any fixable results were produced
170
202
  fixable.length > 0 &&
171
203
  // and that the current parser is able to write
172
- parser.canWrite &&
204
+ this.serializer &&
173
205
  // and that the fixer for this type of IR is avaliable
174
206
  (fixer = this.project.getFixerManager().get(ir.getType()))
175
207
  ) {
@@ -179,9 +211,13 @@ class LintableFile extends DirItem {
179
211
 
180
212
  // check if anything had been applied
181
213
  if (fixes.some((fix) => fix.applied)) {
214
+ // remember that a fix modified the file and that it needs to be
215
+ // written out to disk again after all fixes have been applied
216
+ this.dirty = true;
217
+
182
218
  // fixer should modify the provided IR
183
219
  // so tell current parser to write out the modified representation
184
- parser.write(ir);
220
+ this.sourceFile = this.serializer.serialize(irs);
185
221
 
186
222
  // after writing out the fixed content, we want to reparse to see if any new issues appeared,
187
223
  // while preserving the results that have been fixed so far;
@@ -238,6 +274,37 @@ class LintableFile extends DirItem {
238
274
  return this.irs;
239
275
  }
240
276
 
277
+ /**
278
+ * Set the intermediate representations of this file.
279
+ * @param {Array.<IntermediateRepresentation>} irs the intermediate representations
280
+ * of this file
281
+ */
282
+ setIRs(irs) {
283
+ if (irs.every(ir => ir instanceof IntermediateRepresentation)) {
284
+ this.irs = irs;
285
+ } else {
286
+ // should never happen
287
+ throw new Error("Invalid intermediate representations provided to setIRs");
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Return whether or not the file has been modified since it was last written
293
+ * or since it was read.
294
+ * @returns {boolean} true if the file is dirty, false otherwise
295
+ */
296
+ isDirty() {
297
+ return this.dirty;
298
+ }
299
+
300
+ /**
301
+ * Return the source file of this lintable file.
302
+ * @returns {SourceFile} the source file
303
+ */
304
+ getSourceFile() {
305
+ return this.sourceFile;
306
+ }
307
+
241
308
  /**
242
309
  * Return the stats for the file after issues were found.
243
310
  * @returns {FileStats} the stats for the current file
@@ -253,6 +320,41 @@ class LintableFile extends DirItem {
253
320
  }
254
321
  return fileStats;
255
322
  }
323
+
324
+ /**
325
+ * Return the file type of this file.
326
+ * @returns {FileType} the file type of this file
327
+ */
328
+ getFileType() {
329
+ return this.filetype;
330
+ }
331
+
332
+ /**
333
+ * Apply the available transformers to the intermediate representations of this file.
334
+ * @param {Array.<Result>} results the results of the rules that were applied earlier
335
+ * in the pipeline, or undefined if there are no results or if the rules have not been
336
+ * applied yet
337
+ */
338
+ applyTransformers(results) {
339
+ const transformers = this.filetype.getTransformers();
340
+ if (this.irs && this.irs.length > 0 && transformers && transformers.length > 0) {
341
+ // For each intermediate representation, attempt to apply every transformer.
342
+ // However, only those transformers that have the same type as the intermediate
343
+ // representation can be applied.
344
+ for (let i = 0; i < this.irs.length; i++) {
345
+ if (!this.irs[i]) continue;
346
+ transformers.forEach(transformer => {
347
+ if (this.irs[i].getType() === transformer.getType()) {
348
+ const newIR = transformer.transform(this.irs[i], results);
349
+ if (newIR) {
350
+ this.irs[i] = newIR;
351
+ this.dirty = true;
352
+ }
353
+ }
354
+ });
355
+ }
356
+ }
357
+ }
256
358
  }
257
359
 
258
360
  export default LintableFile;
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * ParserManager.js - Factory to create and return the right parser for the file
3
3
  *
4
- * Copyright © 2022-2024 JEDLSoft
4
+ * Copyright © 2022-2025 JEDLSoft
5
5
  *
6
6
  * Licensed under the Apache License, Version 2.0 (the "License");
7
7
  * you may not use this file except in compliance with the License.
@@ -32,6 +32,21 @@ function getSuperClassName(obj) {
32
32
  * knows about.
33
33
  */
34
34
  class ParserManager {
35
+ /**
36
+ * Information about the parsers that this instance of ilib-lint knows about.
37
+ * Each entry in the object is a parser name and the value is an object
38
+ * with the properties:
39
+ * <ul>
40
+ * <li>description - a description of the parser</li>
41
+ * <li>type - the type of parser</li>
42
+ * <li>extensions - an array of file name extensions that this parser can handle</li>
43
+ * </ul>
44
+ *
45
+ * @type {Object}
46
+ * @private
47
+ */
48
+ parserInfo = {};
49
+
35
50
  /**
36
51
  * Create a new parser manager instance.
37
52
  * @params {Object} options options controlling the construction of this object
@@ -79,13 +94,22 @@ class ParserManager {
79
94
  const p = new parser({
80
95
  getLogger: log4js.getLogger.bind(log4js)
81
96
  });
97
+ const name = p.getName();
98
+ if (this.parserInfo[name]) {
99
+ logger.debug(`Parser ${name} already exists. Cannot add twice. Ignoring.`);
100
+ continue;
101
+ }
102
+ this.parserInfo[name] = {
103
+ description: p.getDescription(),
104
+ type: p.getType(),
105
+ extensions: p.getExtensions()
106
+ };
82
107
  for (const extension of p.getExtensions()) {
83
108
  if (!this.parserCache[extension]) {
84
109
  this.parserCache[extension] = [];
85
110
  }
86
111
  this.parserCache[extension].push(p);
87
112
  }
88
- this.descriptions[p.getName()] = p.getDescription();
89
113
  this.parserByName[p.getName()] = p;
90
114
 
91
115
  logger.trace(`Added parser to the parser manager.`);
@@ -102,10 +126,29 @@ class ParserManager {
102
126
  * @returns {Object} the parser names and descriptions
103
127
  */
104
128
  getDescriptions() {
105
- return this.descriptions;
129
+ let json = {};
130
+ Object.keys(this.parserInfo).forEach((name) => {
131
+ json[name] = this.parserInfo[name].description;
132
+ });
133
+ return json;
134
+ }
135
+
136
+ /**
137
+ * Return the type of the parser with the given name. The type is
138
+ * the type of intermediate represetnation that the parser produces.
139
+ *
140
+ * @param {String} name the name of the parser to get the type for
141
+ * @returns {String} the type of parser with the given name
142
+ */
143
+ getType(name) {
144
+ return this.parserInfo[name].type;
106
145
  }
107
146
 
108
- // for use with the unit tests
147
+ /**
148
+ * Clear the parsers from the factory. This is only intended
149
+ * for use with the unit tests
150
+ * @private
151
+ */
109
152
  clear() {
110
153
  this.parserCache = {};
111
154
  }