-
Notifications
You must be signed in to change notification settings - Fork 683
Expand file tree
/
Copy pathnull-prototype-dictionaries.ts
More file actions
95 lines (83 loc) · 3.71 KB
/
null-prototype-dictionaries.ts
File metadata and controls
95 lines (83 loc) · 3.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { TSESTree, TSESLint, ParserServices } from '@typescript-eslint/utils';
import type * as ts from 'typescript';
type MessageIds = 'error-empty-object-literal-dictionary';
type Options = [];
const nullPrototypeDictionariesRule: TSESLint.RuleModule<MessageIds, Options> = {
defaultOptions: [],
meta: {
type: 'problem',
messages: {
'error-empty-object-literal-dictionary':
'Dictionary objects typed as Record<string, T> should be created using Object.create(null)' +
' instead of an empty object literal. This avoids prototype pollution, collisions with' +
' Object.prototype members such as "toString", and enables higher performance since runtimes' +
' such as V8 process Object.create(null) as opting out of having a hidden class and going' +
' directly to dictionary mode.'
},
schema: [],
docs: {
description:
'Enforce that objects typed as string-keyed dictionaries (Record<string, T>) are instantiated' +
' using Object.create(null) instead of object literals, to avoid prototype pollution issues,' +
' collisions with Object.prototype members such as "toString", and for higher performance' +
' since runtimes such as V8 process Object.create(null) as opting out of having a hidden' +
' class and going directly to dictionary mode',
recommended: 'strict',
url: 'https://www.npmjs.com/package/@rushstack/eslint-plugin'
} as TSESLint.RuleMetaDataDocs
},
create: (context: TSESLint.RuleContext<MessageIds, Options>) => {
const parserServices: Partial<ParserServices> | undefined =
context.sourceCode?.parserServices ?? context.parserServices;
if (!parserServices || !parserServices.program || !parserServices.esTreeNodeToTSNodeMap) {
throw new Error(
'This rule requires your ESLint configuration to define the "parserOptions.project"' +
' property for "@typescript-eslint/parser".'
);
}
const typeChecker: ts.TypeChecker = parserServices.program.getTypeChecker();
/**
* Checks whether the given type represents a pure string-keyed dictionary type:
* it has a string index signature and no named properties.
*/
function isStringKeyedDictionaryType(type: ts.Type): boolean {
// Check if the type has a string index signature
if (!type.getStringIndexType()) {
return false;
}
// A pure dictionary type has no named properties - only an index signature.
// Types with named properties (like interfaces with extra index signatures)
// are not considered pure dictionaries.
if (type.getProperties().length > 0) {
return false;
}
return true;
}
return {
ObjectExpression(node: TSESTree.ObjectExpression): void {
const tsNode: ts.Node = parserServices.esTreeNodeToTSNodeMap!.get(node);
// Get the contextual type (the type expected by the surrounding context,
// e.g. from a variable declaration's type annotation)
const contextualType: ts.Type | undefined = typeChecker.getContextualType(
tsNode as ts.Expression
);
if (!contextualType) {
return;
}
if (!isStringKeyedDictionaryType(contextualType)) {
return;
}
// Only flag empty object literals; non-empty literals are allowed for now
if (node.properties.length === 0) {
context.report({
node,
messageId: 'error-empty-object-literal-dictionary'
});
}
}
};
}
};
export { nullPrototypeDictionariesRule };