@ethlete/eslint-plugin 0.1.0-next.2 → 0.1.0-next.3

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.
@@ -0,0 +1,233 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * @param {string} value
9
+ */
10
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
11
+
12
+ /**
13
+ * @param {any} decorator
14
+ */
15
+ const getDecoratorCall = (decorator) => {
16
+ if (!decorator || decorator.type !== 'Decorator') return null;
17
+
18
+ const expression = decorator.expression;
19
+ if (expression.type !== 'CallExpression') return null;
20
+ if (expression.callee.type !== 'Identifier') return null;
21
+ if (expression.arguments.length === 0) return null;
22
+
23
+ const metadata = expression.arguments[0];
24
+ if (!metadata || metadata.type !== 'ObjectExpression') return null;
25
+
26
+ return {
27
+ name: expression.callee.name,
28
+ metadata,
29
+ };
30
+ };
31
+
32
+ /**
33
+ * @param {any} property
34
+ */
35
+ const getPropertyKeyName = (property) => {
36
+ if (!property || property.computed) return null;
37
+
38
+ const key = property.key;
39
+ if (key.type === 'Identifier') return key.name;
40
+ if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
41
+
42
+ return null;
43
+ };
44
+
45
+ /**
46
+ * @param {any} metadata
47
+ * @param {string} propertyName
48
+ */
49
+ const getMetadataProperty = (metadata, propertyName) => {
50
+ for (const property of metadata.properties) {
51
+ if (!property || property.type !== 'Property' || property.computed) continue;
52
+
53
+ const keyName = getPropertyKeyName(property);
54
+
55
+ if (keyName === propertyName) {
56
+ return property;
57
+ }
58
+ }
59
+
60
+ return null;
61
+ };
62
+
63
+ /**
64
+ * @param {any} node
65
+ */
66
+ const getStringValue = (node) => {
67
+ if (!node) return null;
68
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
69
+
70
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
71
+ return node.quasis[0].value.cooked || '';
72
+ }
73
+
74
+ return null;
75
+ };
76
+
77
+ /**
78
+ * @param {string} filePath
79
+ */
80
+ const readFileIfExists = (filePath) => {
81
+ try {
82
+ if (fs.existsSync(filePath)) {
83
+ return fs.readFileSync(filePath, 'utf8');
84
+ }
85
+ } catch {
86
+ return null;
87
+ }
88
+
89
+ return null;
90
+ };
91
+
92
+ /**
93
+ * @param {import('eslint').Rule.RuleContext} context
94
+ */
95
+ const getContextFilename = (context) => {
96
+ const filename = context.physicalFilename || context.filename;
97
+
98
+ if (!filename || filename === '<input>' || filename === '<text>') {
99
+ return null;
100
+ }
101
+
102
+ return filename;
103
+ };
104
+
105
+ /**
106
+ * @param {string} memberName
107
+ * @param {string | null} template
108
+ */
109
+ const templateReferencesMember = (memberName, template) => {
110
+ if (!template) return false;
111
+
112
+ const escapedName = escapeRegExp(memberName);
113
+ const patterns = [
114
+ new RegExp(`\\{\\{[\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\}\\}`, 'u'),
115
+ new RegExp(`@[a-zA-Z]+\\s*\\([\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\)`, 'u'),
116
+ new RegExp(`@let\\s+[a-zA-Z_$][\\w$]*\\s*=\\s*[\\s\\S]*?\\b${escapedName}\\b`, 'u'),
117
+ new RegExp(`\\*[a-zA-Z0-9_\\-]+\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
118
+ new RegExp(`\\*[a-zA-Z0-9_\\-]+\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
119
+ new RegExp(`\\[[^\\]]+\\]\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
120
+ new RegExp(`\\[[^\\]]+\\]\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
121
+ new RegExp(`\\([^\\)]+\\)\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
122
+ new RegExp(`\\([^\\)]+\\)\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
123
+ ];
124
+
125
+ return patterns.some((pattern) => pattern.test(template));
126
+ };
127
+
128
+ /**
129
+ * @param {string | null} keyName
130
+ */
131
+ const isDynamicHostBindingKey = (keyName) => {
132
+ if (!keyName) return false;
133
+
134
+ return keyName.startsWith('[') || keyName.startsWith('(') || keyName.startsWith('@');
135
+ };
136
+
137
+ /**
138
+ * @param {any} classNode
139
+ */
140
+ const getAngularMetadata = (classNode) => {
141
+ for (const decorator of classNode.decorators || []) {
142
+ const call = getDecoratorCall(decorator);
143
+ if (!call) continue;
144
+
145
+ if (call.name === 'Component' || call.name === 'Directive') {
146
+ return call;
147
+ }
148
+ }
149
+
150
+ return null;
151
+ };
152
+
153
+ /**
154
+ * @param {string} memberName
155
+ * @param {string | null} text
156
+ */
157
+ const textReferencesMember = (memberName, text) => {
158
+ if (!text) return false;
159
+
160
+ const pattern = new RegExp(`\\b${escapeRegExp(memberName)}\\b`, 'u');
161
+ return pattern.test(text);
162
+ };
163
+
164
+ /**
165
+ * @param {string} memberName
166
+ * @param {any} metadata
167
+ * @param {import('eslint').Rule.RuleContext} context
168
+ */
169
+ const isReferencedFromTemplateOrHostMetadata = (memberName, metadata, context) => {
170
+ const templateProperty = getMetadataProperty(metadata, 'template');
171
+ if (templateProperty && templateReferencesMember(memberName, getStringValue(templateProperty.value))) {
172
+ return true;
173
+ }
174
+
175
+ const templateUrlProperty = getMetadataProperty(metadata, 'templateUrl');
176
+ if (templateUrlProperty) {
177
+ const templateUrl = getStringValue(templateUrlProperty.value);
178
+ const filename = getContextFilename(context);
179
+
180
+ if (templateUrl && filename) {
181
+ const templatePath = path.resolve(path.dirname(filename), templateUrl);
182
+ const templateText = readFileIfExists(templatePath);
183
+ if (templateReferencesMember(memberName, templateText)) {
184
+ return true;
185
+ }
186
+ }
187
+ }
188
+
189
+ const hostProperty = getMetadataProperty(metadata, 'host');
190
+ if (hostProperty && hostProperty.value.type === 'ObjectExpression') {
191
+ for (const property of hostProperty.value.properties) {
192
+ if (!property || property.type !== 'Property') continue;
193
+ if (!isDynamicHostBindingKey(getPropertyKeyName(property))) continue;
194
+
195
+ if (textReferencesMember(memberName, getStringValue(property.value))) {
196
+ return true;
197
+ }
198
+ }
199
+ }
200
+
201
+ return false;
202
+ };
203
+
204
+ /**
205
+ * @param {any} node
206
+ */
207
+ const getMemberName = (node) => {
208
+ if (node.key?.type === 'Identifier') return node.key.name;
209
+ if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
210
+
211
+ return null;
212
+ };
213
+
214
+ /**
215
+ * @param {any} classNode
216
+ * @param {string} memberName
217
+ * @param {import('eslint').Rule.RuleContext} context
218
+ */
219
+ const isReferencedFromTemplateOrHost = (classNode, memberName, context) => {
220
+ const angularMetadata = getAngularMetadata(classNode);
221
+
222
+ if (!angularMetadata) {
223
+ return false;
224
+ }
225
+
226
+ return isReferencedFromTemplateOrHostMetadata(memberName, angularMetadata.metadata, context);
227
+ };
228
+
229
+ module.exports = {
230
+ getAngularMetadata,
231
+ getMemberName,
232
+ isReferencedFromTemplateOrHost,
233
+ };
@@ -0,0 +1,294 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const ts = require('typescript');
7
+
8
+ /** @type {Map<string, import('typescript').SourceFile | null>} */
9
+ const SOURCE_FILE_CACHE = new Map();
10
+
11
+ /**
12
+ * @param {string} filePath
13
+ */
14
+ const readFileIfExists = (filePath) => {
15
+ try {
16
+ if (fs.existsSync(filePath)) {
17
+ return fs.readFileSync(filePath, 'utf8');
18
+ }
19
+ } catch {
20
+ return null;
21
+ }
22
+
23
+ return null;
24
+ };
25
+
26
+ /**
27
+ * @param {{ filePath: string, text?: string | null }} config
28
+ */
29
+ const getSourceFile = ({ filePath, text = null }) => {
30
+ if (text !== null) {
31
+ return ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
32
+ }
33
+
34
+ if (SOURCE_FILE_CACHE.has(filePath)) {
35
+ return SOURCE_FILE_CACHE.get(filePath) ?? null;
36
+ }
37
+
38
+ const fileText = readFileIfExists(filePath);
39
+
40
+ if (fileText === null) {
41
+ SOURCE_FILE_CACHE.set(filePath, null);
42
+ return null;
43
+ }
44
+
45
+ const sourceFile = ts.createSourceFile(filePath, fileText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
46
+ SOURCE_FILE_CACHE.set(filePath, sourceFile);
47
+ return sourceFile;
48
+ };
49
+
50
+ /**
51
+ * @param {string} importingFilePath
52
+ * @param {string} specifier
53
+ */
54
+ const resolveRelativeModule = (importingFilePath, specifier) => {
55
+ if (!specifier.startsWith('.')) {
56
+ return null;
57
+ }
58
+
59
+ const basePath = path.resolve(path.dirname(importingFilePath), specifier);
60
+ const candidates = [
61
+ `${basePath}.ts`,
62
+ `${basePath}.tsx`,
63
+ `${basePath}.d.ts`,
64
+ path.join(basePath, 'index.ts'),
65
+ path.join(basePath, 'index.d.ts'),
66
+ ];
67
+
68
+ for (const candidate of candidates) {
69
+ if (fs.existsSync(candidate)) {
70
+ return candidate;
71
+ }
72
+ }
73
+
74
+ return null;
75
+ };
76
+
77
+ /**
78
+ * @param {import('typescript').TypeElement | import('typescript').ClassElement} member
79
+ */
80
+ const getMemberName = (member) => {
81
+ if (!member.name) return null;
82
+
83
+ if (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) {
84
+ return member.name.text;
85
+ }
86
+
87
+ if (ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)) {
88
+ return String(member.name.text);
89
+ }
90
+
91
+ return null;
92
+ };
93
+
94
+ /**
95
+ * @param {{ sourceFile: import('typescript').SourceFile, typeName: string, currentFilePath: string, visited: Set<string>, currentFileText?: string | null }} config
96
+ */
97
+ const collectMemberNamesForType = ({ sourceFile, typeName, currentFilePath, visited, currentFileText = null }) => {
98
+ const visitKey = `${sourceFile.fileName}::${typeName}`;
99
+ if (visited.has(visitKey)) {
100
+ return new Set();
101
+ }
102
+
103
+ visited.add(visitKey);
104
+
105
+ /** @type {Set<string>} */
106
+ const memberNames = new Set();
107
+
108
+ /**
109
+ * @param {import('typescript').TypeNode | undefined} typeNode
110
+ * @param {string} filePath
111
+ * @param {string | null} fileText
112
+ */
113
+ const collectFromTypeNode = (typeNode, filePath, fileText) => {
114
+ if (!typeNode) {
115
+ return;
116
+ }
117
+
118
+ if (ts.isTypeLiteralNode(typeNode)) {
119
+ for (const member of typeNode.members) {
120
+ const memberName = getMemberName(member);
121
+ if (memberName) memberNames.add(memberName);
122
+ }
123
+
124
+ return;
125
+ }
126
+
127
+ if (ts.isIntersectionTypeNode(typeNode)) {
128
+ for (const part of typeNode.types) {
129
+ collectFromTypeNode(part, filePath, fileText);
130
+ }
131
+
132
+ return;
133
+ }
134
+
135
+ if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
136
+ const referencedMembers = collectMemberNamesForTypeByName({
137
+ sourceFile: getSourceFile({ filePath, text: fileText }),
138
+ typeName: typeNode.typeName.text,
139
+ currentFilePath: filePath,
140
+ visited,
141
+ currentFileText: fileText,
142
+ });
143
+
144
+ for (const referencedMember of referencedMembers) {
145
+ memberNames.add(referencedMember);
146
+ }
147
+ }
148
+ };
149
+
150
+ const statements = sourceFile.statements;
151
+
152
+ for (const statement of statements) {
153
+ if (ts.isInterfaceDeclaration(statement) && statement.name.text === typeName) {
154
+ for (const member of statement.members) {
155
+ const memberName = getMemberName(member);
156
+ if (memberName) memberNames.add(memberName);
157
+ }
158
+
159
+ if (statement.heritageClauses) {
160
+ for (const clause of statement.heritageClauses) {
161
+ if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
162
+
163
+ for (const heritageType of clause.types) {
164
+ if (!ts.isIdentifier(heritageType.expression)) continue;
165
+
166
+ const inheritedMembers = collectMemberNamesForTypeByName({
167
+ sourceFile,
168
+ typeName: heritageType.expression.text,
169
+ currentFilePath,
170
+ visited,
171
+ currentFileText,
172
+ });
173
+
174
+ for (const inheritedMember of inheritedMembers) {
175
+ memberNames.add(inheritedMember);
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ return memberNames;
182
+ }
183
+
184
+ if (ts.isTypeAliasDeclaration(statement) && statement.name.text === typeName) {
185
+ collectFromTypeNode(
186
+ statement.type,
187
+ sourceFile.fileName,
188
+ sourceFile.fileName === currentFilePath ? currentFileText : null,
189
+ );
190
+ return memberNames;
191
+ }
192
+ }
193
+
194
+ for (const statement of statements) {
195
+ if (!ts.isImportDeclaration(statement)) continue;
196
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
197
+ if (!statement.importClause || !statement.importClause.namedBindings) continue;
198
+ if (!ts.isNamedImports(statement.importClause.namedBindings)) continue;
199
+
200
+ for (const element of statement.importClause.namedBindings.elements) {
201
+ const localName = element.name.text;
202
+ if (localName !== typeName) continue;
203
+
204
+ const importedName = element.propertyName ? element.propertyName.text : element.name.text;
205
+ const resolvedModulePath = resolveRelativeModule(sourceFile.fileName, statement.moduleSpecifier.text);
206
+ if (!resolvedModulePath) {
207
+ return memberNames;
208
+ }
209
+
210
+ const importedSourceFile = getSourceFile({ filePath: resolvedModulePath });
211
+ if (!importedSourceFile) {
212
+ return memberNames;
213
+ }
214
+
215
+ const importedMembers = collectMemberNamesForTypeByName({
216
+ sourceFile: importedSourceFile,
217
+ typeName: importedName,
218
+ currentFilePath: resolvedModulePath,
219
+ visited,
220
+ });
221
+
222
+ for (const importedMember of importedMembers) {
223
+ memberNames.add(importedMember);
224
+ }
225
+
226
+ return memberNames;
227
+ }
228
+ }
229
+
230
+ return memberNames;
231
+ };
232
+
233
+ /**
234
+ * @param {{ sourceFile: import('typescript').SourceFile | null, typeName: string, currentFilePath: string, visited: Set<string>, currentFileText?: string | null }} config
235
+ */
236
+ const collectMemberNamesForTypeByName = ({
237
+ sourceFile,
238
+ typeName,
239
+ currentFilePath,
240
+ visited,
241
+ currentFileText = null,
242
+ }) => {
243
+ if (!sourceFile) {
244
+ return new Set();
245
+ }
246
+
247
+ return collectMemberNamesForType({
248
+ sourceFile,
249
+ typeName,
250
+ currentFilePath,
251
+ visited,
252
+ currentFileText,
253
+ });
254
+ };
255
+
256
+ /**
257
+ * @param {{ classNode: any, context: import('eslint').Rule.RuleContext }} config
258
+ */
259
+ const getImplementedContractMemberNames = ({ classNode, context }) => {
260
+ const filename = context.physicalFilename || context.filename;
261
+ if (!filename || filename === '<input>' || filename === '<text>') {
262
+ return new Set();
263
+ }
264
+
265
+ const currentSourceFile = getSourceFile({ filePath: filename, text: context.sourceCode.text });
266
+ if (!currentSourceFile) {
267
+ return new Set();
268
+ }
269
+
270
+ /** @type {Set<string>} */
271
+ const memberNames = new Set();
272
+
273
+ for (const implementedType of classNode.implements || []) {
274
+ if (!implementedType.expression || implementedType.expression.type !== 'Identifier') continue;
275
+
276
+ const typeMemberNames = collectMemberNamesForTypeByName({
277
+ sourceFile: currentSourceFile,
278
+ typeName: implementedType.expression.name,
279
+ currentFilePath: filename,
280
+ visited: new Set(),
281
+ currentFileText: context.sourceCode.text,
282
+ });
283
+
284
+ for (const memberName of typeMemberNames) {
285
+ memberNames.add(memberName);
286
+ }
287
+ }
288
+
289
+ return memberNames;
290
+ };
291
+
292
+ module.exports = {
293
+ getImplementedContractMemberNames,
294
+ };
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getMemberName, isReferencedFromTemplateOrHost } = require('./internals/angular-member-visibility');
5
+
4
6
  /**
5
7
  * @param {any} node
6
8
  */
@@ -23,24 +25,57 @@ const noPublicProperty = {
23
25
  messages: {
24
26
  noPublicProperty:
25
27
  'No public keyword on class properties. The only exception is an injected member that intentionally exposes an external API.',
28
+ shouldBeProtected:
29
+ 'Member `{{name}}` should be protected instead of public because it is referenced from a template or host binding.',
26
30
  },
27
31
  },
28
32
  create(context) {
33
+ /**
34
+ * @param {any} node
35
+ */
36
+ const removePublicFix = (node) => {
37
+ const publicToken = context.sourceCode.getFirstToken(node, (token) => token.value === 'public');
38
+ const nextToken = publicToken ? context.sourceCode.getTokenAfter(publicToken) : null;
39
+ if (!publicToken || !nextToken) return null;
40
+
41
+ return (fixer) => fixer.removeRange([publicToken.range[0], nextToken.range[0]]);
42
+ };
43
+
44
+ /**
45
+ * @param {any} node
46
+ */
47
+ const replacePublicWithProtectedFix = (node) => {
48
+ const publicToken = context.sourceCode.getFirstToken(node, (token) => token.value === 'public');
49
+ if (!publicToken) return null;
50
+
51
+ return (fixer) => fixer.replaceText(publicToken, 'protected');
52
+ };
53
+
29
54
  return {
30
55
  PropertyDefinition(node) {
31
56
  if (node.accessibility !== 'public') return;
32
57
  if (isInjectCall(node.value)) return;
33
58
 
59
+ const classNode = node.parent && node.parent.type === 'ClassBody' ? node.parent.parent : null;
60
+ const memberName = getMemberName(node);
61
+ const isTemplateVisible =
62
+ classNode && memberName ? isReferencedFromTemplateOrHost(classNode, memberName, context) : false;
63
+
64
+ if (isTemplateVisible) {
65
+ context.report({
66
+ node,
67
+ messageId: 'shouldBeProtected',
68
+ data: { name: memberName },
69
+ fix: replacePublicWithProtectedFix(node),
70
+ });
71
+
72
+ return;
73
+ }
74
+
34
75
  context.report({
35
76
  node,
36
77
  messageId: 'noPublicProperty',
37
- fix(fixer) {
38
- const publicToken = context.sourceCode.getFirstToken(node, (token) => token.value === 'public');
39
- const nextToken = publicToken ? context.sourceCode.getTokenAfter(publicToken) : null;
40
- if (!publicToken || !nextToken) return null;
41
-
42
- return fixer.removeRange([publicToken.range[0], nextToken.range[0]]);
43
- },
78
+ fix: removePublicFix(node),
44
79
  });
45
80
  },
46
81
  };
@@ -0,0 +1,94 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * @param {import('eslint').SourceCode} sourceCode
6
+ * @param {import('eslint').Rule.Node} node
7
+ */
8
+ const getAttachedInternalComment = (sourceCode, node) => {
9
+ const comments = sourceCode.getCommentsBefore(node);
10
+ const attachedComment = comments[comments.length - 1];
11
+
12
+ if (!attachedComment || attachedComment.type !== 'Block') {
13
+ return null;
14
+ }
15
+
16
+ const textBetween = sourceCode.text.slice(attachedComment.range[1], node.range[0]);
17
+
18
+ if (textBetween.trim() !== '') {
19
+ return null;
20
+ }
21
+
22
+ return /@internal\b/u.test(attachedComment.value) ? attachedComment : null;
23
+ };
24
+
25
+ /**
26
+ * @param {any} node
27
+ */
28
+ const getMemberName = (node) => {
29
+ if (node.key?.type === 'Identifier') return node.key.name;
30
+ if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
31
+
32
+ return 'member';
33
+ };
34
+
35
+ /**
36
+ * @param {any} node
37
+ */
38
+ const getMemberKind = (node) => {
39
+ if (node.type === 'MethodDefinition') return 'method';
40
+ return 'property';
41
+ };
42
+
43
+ /** @type {import('eslint').Rule.RuleModule} */
44
+ const noRedundantInternal = {
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: {
48
+ description: 'Disallow redundant @internal usage on class members that are already hidden by accessibility.',
49
+ recommended: true,
50
+ },
51
+ fixable: 'code',
52
+ schema: [],
53
+ messages: {
54
+ redundantInternal:
55
+ '`@internal` is redundant on {{kind}} `{{name}}` because {{accessibility}} members are already hidden from consumers.',
56
+ },
57
+ },
58
+ create(context) {
59
+ const sourceCode = context.sourceCode;
60
+
61
+ /**
62
+ * @param {any} node
63
+ */
64
+ const checkMember = (node) => {
65
+ const internalComment = getAttachedInternalComment(sourceCode, node);
66
+
67
+ if (!internalComment) {
68
+ return;
69
+ }
70
+
71
+ if (node.accessibility !== 'private' && node.accessibility !== 'protected') {
72
+ return;
73
+ }
74
+
75
+ context.report({
76
+ node: internalComment,
77
+ messageId: 'redundantInternal',
78
+ data: {
79
+ accessibility: node.accessibility,
80
+ kind: getMemberKind(node),
81
+ name: getMemberName(node),
82
+ },
83
+ fix: (fixer) => fixer.removeRange([internalComment.range[0], node.range[0]]),
84
+ });
85
+ };
86
+
87
+ return {
88
+ PropertyDefinition: checkMember,
89
+ MethodDefinition: checkMember,
90
+ };
91
+ },
92
+ };
93
+
94
+ module.exports = noRedundantInternal;