@eagleoutice/eslint-config-flowr 2.1.10 → 2.1.11

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
@@ -27,6 +27,7 @@ below `test/`.
27
27
  | :-- | :-- | :-- |
28
28
  | `flowr/use-instead` | the `@useInstead` tag | a reference to what a helper object replaced |
29
29
  | `flowr/replacement-pattern` | [`patterns.ts`](patterns.ts) | a shape of code with a helper equivalent |
30
+ | `flowr/no-semantic-widening` | the types | a value of a `__brand`ed type (`NodeId`, `BrandedIdentifier`, ...) that silently becomes a plain `string` or `number`, also within `string[]`, `Set<string>`, or `Map<string, ...>` |
30
31
 
31
32
  Both fix in place when every name the replacement needs is already in scope, and offer an editor suggestion otherwise,
32
33
  since a fixer cannot add the missing import. A type-only binding of the right name does not count.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eagleoutice/eslint-config-flowr",
3
- "version": "2.1.10",
3
+ "version": "2.1.11",
4
4
  "description": "Linting rules for flowr and friends",
5
5
  "license": "ISC",
6
6
  "repository": {
package/plugin.d.ts CHANGED
@@ -6,6 +6,7 @@ declare const plugin: {
6
6
  'use-instead': import("eslint").Rule.RuleModule;
7
7
  'replacement-pattern': import("eslint").Rule.RuleModule;
8
8
  'file-naming': import("eslint").Rule.RuleModule;
9
+ 'no-semantic-widening': import("eslint").Rule.RuleModule;
9
10
  };
10
11
  };
11
12
  export = plugin;
package/plugin.js CHANGED
@@ -6,12 +6,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  const replacement_pattern_1 = __importDefault(require("./rules/replacement-pattern"));
7
7
  const use_instead_1 = __importDefault(require("./rules/use-instead"));
8
8
  const file_naming_1 = __importDefault(require("./rules/file-naming"));
9
+ const no_semantic_widening_1 = __importDefault(require("./rules/no-semantic-widening"));
9
10
  const plugin = {
10
11
  meta: { name: '@eagleoutice/eslint-config-flowr' },
11
12
  rules: {
12
13
  'use-instead': use_instead_1.default,
13
14
  'replacement-pattern': replacement_pattern_1.default,
14
- 'file-naming': file_naming_1.default
15
+ 'file-naming': file_naming_1.default,
16
+ 'no-semantic-widening': no_semantic_widening_1.default
15
17
  }
16
18
  };
17
19
  module.exports = plugin;
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export = rule;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ const util_1 = require("./util");
3
+ /* ts.TypeFlags and ts.SymbolFlags, spelled out to keep typescript out of the dependencies */
4
+ const StringFlag = 1 << 2;
5
+ const NumberFlag = 1 << 3;
6
+ const UndefinedFlag = 1 << 15;
7
+ const NullFlag = 1 << 16;
8
+ const TypeParameterFlag = 1 << 18;
9
+ const BrandProperty = '__brand';
10
+ const Collections = new Set(['Array', 'ReadonlyArray', 'Set', 'ReadonlySet', 'Map', 'ReadonlyMap', 'DefaultMap']);
11
+ function membersOf(type) {
12
+ return (type.isUnion() ? type.types : [type]).filter(t => (t.flags & (UndefinedFlag | NullFlag)) === 0);
13
+ }
14
+ /** A semantic type carries a brand: `string & { __brand?: 'node-id' }`. */
15
+ function isSemantic(type, checker) {
16
+ const members = membersOf(type);
17
+ return members.length > 0 && members.every(t => checker.getPropertyOfType(t, BrandProperty) !== undefined);
18
+ }
19
+ function isPlainPrimitive(type, checker) {
20
+ const members = membersOf(type);
21
+ return members.length > 0 && members.every(t => (t.flags & (StringFlag | NumberFlag)) !== 0 && checker.getPropertyOfType(t, BrandProperty) === undefined);
22
+ }
23
+ function collectionArguments(type, checker) {
24
+ const [member] = membersOf(type);
25
+ const name = member?.getSymbol()?.getName();
26
+ return name !== undefined && Collections.has(name) ? checker.getTypeArguments(member) : undefined;
27
+ }
28
+ /** Whether a value of type `source` loses its meaning in a place of type `target`, on its own or inside a collection. */
29
+ function widens(source, target, checker) {
30
+ if (isSemantic(source, checker)) {
31
+ return isPlainPrimitive(target, checker);
32
+ }
33
+ const from = collectionArguments(source, checker);
34
+ const to = collectionArguments(target, checker);
35
+ return from !== undefined && to !== undefined && from.some((f, i) => to[i] !== undefined && widens(f, to[i], checker));
36
+ }
37
+ /** A parameter a library declares as a plain string takes any string, while one of a collection (`Set<string>`) states what the code chose. */
38
+ function acceptedByLibrary(call, argument, tsCall, checker) {
39
+ if (call.type !== 'CallExpression' && call.type !== 'NewExpression') {
40
+ return false;
41
+ }
42
+ const declaration = checker.getResolvedSignature(tsCall)?.declaration;
43
+ if (declaration?.getSourceFile()?.fileName.includes('/node_modules/') !== true) {
44
+ return false;
45
+ }
46
+ const parameters = declaration.parameters;
47
+ const parameter = parameters?.[Math.min(call.arguments.indexOf(argument), parameters.length - 1)];
48
+ return parameter !== undefined && (checker.getTypeAtLocation(parameter).flags & TypeParameterFlag) === 0;
49
+ }
50
+ const rule = {
51
+ meta: {
52
+ type: 'problem',
53
+ docs: { description: 'a value of a semantic type (NodeId, BrandedIdentifier, ...) must not silently become a plain string or number' },
54
+ messages: {
55
+ widens: 'This turns a {{source}} into a plain {{target}}. Use a semantic type for what receives it, or cast explicitly if the meaning really ends here.'
56
+ },
57
+ schema: []
58
+ },
59
+ create(context) {
60
+ const services = (0, util_1.typeServices)(context);
61
+ if (!services) {
62
+ return {};
63
+ }
64
+ const checker = services.program.getTypeChecker();
65
+ const check = (node) => {
66
+ if (!node || node.type === 'SpreadElement') {
67
+ return;
68
+ }
69
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
70
+ const target = checker.getContextualType(tsNode);
71
+ if (target === undefined) {
72
+ return;
73
+ }
74
+ const source = checker.getTypeAtLocation(tsNode);
75
+ if (!widens(source, target, checker) || (0, util_1.suppressed)(context, node, ['no-semantic-widening'])) {
76
+ return;
77
+ }
78
+ const parent = node.parent;
79
+ if (acceptedByLibrary(parent, node, services.esTreeNodeToTSNodeMap.get(parent), checker)) {
80
+ return;
81
+ }
82
+ context.report({ node, messageId: 'widens', data: { source: checker.typeToString(source), target: checker.typeToString(target) } });
83
+ };
84
+ const checkArguments = (call) => call.arguments.forEach(check);
85
+ return {
86
+ CallExpression: checkArguments,
87
+ NewExpression: checkArguments,
88
+ ReturnStatement: node => check(node.argument),
89
+ VariableDeclarator(node) {
90
+ check(node.init);
91
+ },
92
+ AssignmentExpression(node) {
93
+ if (node.operator === '=') {
94
+ check(node.right);
95
+ }
96
+ },
97
+ Property(node) {
98
+ check(node.value);
99
+ },
100
+ ArrowFunctionExpression(node) {
101
+ if (node.body.type !== 'BlockStatement') {
102
+ check(node.body);
103
+ }
104
+ }
105
+ };
106
+ }
107
+ };
108
+ module.exports = rule;
@@ -23,12 +23,30 @@ export interface TsSymbol {
23
23
  getJsDocTags(checker: TsTypeChecker): readonly TsJsDocTag[];
24
24
  }
25
25
  export interface TsType {
26
+ flags: number;
27
+ types?: readonly TsType[];
26
28
  getProperties(): readonly TsSymbol[];
29
+ getSymbol(): TsSymbol | undefined;
30
+ isUnion(): this is TsType & {
31
+ types: readonly TsType[];
32
+ };
33
+ }
34
+ /** What a call resolves to, its parameters only for a declaration in code. */
35
+ export interface TsSignature {
36
+ declaration?: TsDeclaration & {
37
+ parameters?: readonly unknown[];
38
+ };
27
39
  }
28
40
  export interface TsTypeChecker {
29
41
  getSymbolAtLocation(node: unknown): TsSymbol | undefined;
30
42
  getAliasedSymbol(symbol: TsSymbol): TsSymbol;
31
43
  getTypeOfSymbolAtLocation(symbol: TsSymbol, location: TsDeclaration): TsType;
44
+ getTypeAtLocation(node: unknown): TsType;
45
+ getContextualType(node: unknown): TsType | undefined;
46
+ getPropertyOfType(type: TsType, name: string): TsSymbol | undefined;
47
+ getTypeArguments(type: TsType): readonly TsType[];
48
+ getResolvedSignature(node: unknown): TsSignature | undefined;
49
+ typeToString(type: TsType): string;
32
50
  /** what a module exports, its symbol taken from the source file */
33
51
  getExportsOfModule(module: TsSymbol): readonly TsSymbol[];
34
52
  /** the variable a shorthand property `{ framesOf }` stands for */