-
Notifications
You must be signed in to change notification settings - Fork 878
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[eslint-plugin] Add attribute names rule (#4516)
- Loading branch information
1 parent
9c7dba2
commit c51bc18
Showing
15 changed files
with
321 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@lit-labs/testing': patch | ||
--- | ||
|
||
Update @web/test-runner-commands dependency |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/** | ||
* @license | ||
* Copyright 2024 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
import {AbsolutePath, Analyzer} from '@lit-labs/analyzer'; | ||
import { | ||
ESLintUtils, | ||
ParserServicesWithTypeInformation, | ||
} from '@typescript-eslint/utils'; | ||
// TODO (justinfagnani): get from ESLintUtils if possible? | ||
// See q: https://discord.com/channels/1026804805894672454/1084238921677946992/1199174067459198986 | ||
import ts from 'typescript'; | ||
import * as path from 'path'; | ||
|
||
export const createRule = ESLintUtils.RuleCreator( | ||
// TODO (justinfagnani): set up rule doc publishing pipeline for lit.dev | ||
(name: string) => `https://lit.dev/eslint-plugin/${name}` | ||
); | ||
|
||
const analyzerByProgram = new WeakMap<ts.Program, Analyzer>(); | ||
|
||
export const getAnalyzer = (services: ParserServicesWithTypeInformation) => { | ||
const program = services.program; | ||
let analyzer = analyzerByProgram.get(program); | ||
if (analyzer === undefined) { | ||
analyzer = new Analyzer({ | ||
typescript: ts, | ||
getProgram: () => program, | ||
fs: ts.sys, | ||
path, | ||
}); | ||
} | ||
return analyzer; | ||
}; | ||
|
||
export const getDeclarationForNode = ( | ||
analyzer: Analyzer, | ||
filename: string, | ||
node: ts.Node | ||
) => { | ||
const modulePath = analyzer.fs.useCaseSensitiveFileNames | ||
? filename | ||
: filename.toLocaleLowerCase(); | ||
const module = analyzer.getModule(modulePath as AbsolutePath); | ||
for (const declaration of module.declarations) { | ||
if (declaration.node === node) { | ||
return declaration; | ||
} | ||
} | ||
return undefined; | ||
}; | ||
|
||
export const isTrueLiteral = (e: ts.Expression): e is ts.TrueLiteral => | ||
e.kind === ts.SyntaxKind.TrueKeyword; | ||
|
||
export const isFalseLiteral = (e: ts.Expression): e is ts.FalseLiteral => | ||
e.kind === ts.SyntaxKind.FalseKeyword; | ||
|
||
// TODO (justinfagnani): add a type predicate? | ||
// When is `undefined` ever not an identifier? | ||
export const isUndefinedLiteral = (e: ts.Expression) => | ||
(ts.isIdentifier(e) && e.text === 'undefined') || | ||
e.kind === ts.SyntaxKind.UndefinedKeyword; |
111 changes: 111 additions & 0 deletions
111
packages/labs/eslint-plugin/src/rules/attribute-names.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/** | ||
* @license | ||
* Copyright 2024 Google LLC | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
*/ | ||
|
||
/** | ||
* @fileoverview Enforces attribute naming conventions | ||
*/ | ||
|
||
import {ESLintUtils} from '@typescript-eslint/utils'; | ||
import ts from 'typescript'; | ||
import { | ||
createRule, | ||
getAnalyzer, | ||
getDeclarationForNode, | ||
isFalseLiteral, | ||
isTrueLiteral, | ||
isUndefinedLiteral, | ||
} from '../lib/util.js'; | ||
|
||
export const attributeNames = createRule({ | ||
name: 'attribute-names', | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Enforces attribute naming conventions', | ||
recommended: 'recommended', | ||
}, | ||
schema: [], | ||
messages: { | ||
casedAttribute: | ||
'Attributes are case-insensitive and therefore should be ' + | ||
'defined in lower case', | ||
casedPropertyWithoutAttribute: | ||
'Property has non-lowercase casing but no attribute. It should ' + | ||
'instead have an explicit `attribute` set to the lower case ' + | ||
'name (usually snake-case)', | ||
}, | ||
}, | ||
|
||
create(context) { | ||
const services = ESLintUtils.getParserServices(context); | ||
const filename = context.filename; | ||
const analyzer = getAnalyzer(services); | ||
|
||
return { | ||
ClassDeclaration(node) { | ||
const tsNode = services.esTreeNodeToTSNodeMap.get(node); | ||
const declaration = getDeclarationForNode(analyzer, filename, tsNode); | ||
if ( | ||
declaration === undefined || | ||
!declaration.isLitElementDeclaration() | ||
) { | ||
return; | ||
} | ||
const properties = declaration.reactiveProperties; | ||
|
||
for (const [propertyName, property] of properties.entries()) { | ||
const attributeOptionNode = property.optionsNode?.properties.find( | ||
(p): p is ts.PropertyAssignment => | ||
ts.isPropertyAssignment(p) && | ||
ts.isIdentifier(p.name) && | ||
p.name.text === 'attribute' | ||
); | ||
|
||
// TODO (justinfagnani): handle other statically known truthy and | ||
// undefined values? This would let attribute names be in const | ||
// variables, etc., but still be lintable. | ||
if ( | ||
attributeOptionNode === undefined || | ||
isTrueLiteral(attributeOptionNode.initializer) || | ||
isUndefinedLiteral(attributeOptionNode.initializer) | ||
) { | ||
if (propertyName.toLowerCase() !== propertyName) { | ||
// Report on the whole property declaration, since there's no | ||
// attribute option | ||
const estreeNode = services.tsNodeToESTreeNodeMap.get( | ||
property.node | ||
); | ||
context.report({ | ||
node: estreeNode, | ||
messageId: 'casedPropertyWithoutAttribute', | ||
}); | ||
} | ||
} else if (isFalseLiteral(attributeOptionNode.initializer)) { | ||
continue; | ||
} else if (ts.isStringLiteral(attributeOptionNode.initializer)) { | ||
const attributeName = attributeOptionNode.initializer.text; | ||
if (attributeName.toLowerCase() !== attributeName) { | ||
// Report on just the attribute option | ||
const estreeNode = | ||
services.tsNodeToESTreeNodeMap.get(attributeOptionNode); | ||
context.report({ | ||
node: estreeNode, | ||
messageId: 'casedAttribute', | ||
}); | ||
} | ||
} else { | ||
// Unsupported attribute option. | ||
// TODO (justinfagnani): Report? | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
defaultOptions: [] as ReadonlyArray<unknown>, | ||
}); | ||
|
||
// TODO (justinfagnani): Is this necessary? | ||
export default attributeNames; |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.