Skip to content

Commit

Permalink
Adds Name Shadowing lint rule (#124)
Browse files Browse the repository at this point in the history
* Adds Name Shadowing lint rule

* Refined messaging for shadowing a global function
  • Loading branch information
markwpearce committed Jul 30, 2024
1 parent 4581ee9 commit fa6a65c
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type BsLintConfig = Pick<BsConfig, 'project' | 'rootDir' | 'files' | 'cwd
'color-cert'?: RuleColorCertCompliant;
'no-assocarray-component-field-type'?: RuleSeverity;
'no-array-component-field-type'?: RuleSeverity;
'name-shadowing'?: RuleSeverity;
};
globals?: string[];
ignores?: string[];
Expand Down Expand Up @@ -86,6 +87,7 @@ export interface BsLintRules {
colorCertCompliant: RuleColorCertCompliant;
noAssocarrayComponentFieldType: BsLintSeverity;
noArrayComponentFieldType: BsLintSeverity;
nameShadowing: BsLintSeverity;
}

export { Linter };
Expand Down
9 changes: 8 additions & 1 deletion src/plugins/codeStyle/diagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export enum CodeStyleError {
ColorAlphaDefaults = 'LINT3022',
ColorCertCompliant = 'LINT3023',
NoAssocarrayFieldType = 'LINT3024',
NoArrayFieldType = 'LINT3025'
NoArrayFieldType = 'LINT3025',
NameShadowing = 'LINT3026'
}

const CS = 'Code style:';
Expand Down Expand Up @@ -215,5 +216,11 @@ export const messages = {
severity: severity,
source: 'bslint',
range
}),
nameShadowing: (thisThingKind: string, thatThingKind: string, thatThingName: string, severity: DiagnosticSeverity) => ({
message: `${ST} ${thisThingKind} has same name as ${thatThingKind ? thatThingKind + ' ' : ''}'${thatThingName}'`,
code: CodeStyleError.NameShadowing,
severity: severity,
source: 'bslint'
})
};
55 changes: 55 additions & 0 deletions src/plugins/codeStyle/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,61 @@ describe('codeStyle', () => {
});
});

describe('name-shadowing', () => {
it('detects name shadowings', async () => {
const diagnostics = await linter.run({
...project1,
files: ['source/name-shadowing.bs'],
rules: {
'name-shadowing': 'error'
}
});
expectDiagnosticsFmt(diagnostics, [
'15:LINT3026:Strictness: Class has same name as Class \'TestClass\'',
'18:LINT3026:Strictness: Enum has same name as Enum \'TestEnum\'',
'21:LINT3026:Strictness: Interface has same name as Interface \'TestInterface\'',
'24:LINT3026:Strictness: Const has same name as Const \'TestConst\'',
'26:LINT3026:Strictness: Const has same name as Namespace \'TestNamespace\''
]);
});

it('detects name shadowings across scope', async () => {
const diagnostics = await linter.run({
...project1,
files: ['source/name-shadowing.bs', 'source/name-shadowing-import.bs'],
rules: {
'name-shadowing': 'error'
}
});
expectDiagnosticsFmt(diagnostics, [
'02:LINT3026:Strictness: Class has same name as Class \'TestImportClass\'',
'05:LINT3026:Strictness: Enum has same name as Enum \'TestImportEnum\'',
'08:LINT3026:Strictness: Interface has same name as Interface \'TestImportInterface\'',
'11:LINT3026:Strictness: Const has same name as Const \'TestImportConst\'',
'15:LINT3026:Strictness: Class has same name as Class \'TestClass\'',
'18:LINT3026:Strictness: Enum has same name as Enum \'TestEnum\'',
'21:LINT3026:Strictness: Interface has same name as Interface \'TestInterface\'',
'24:LINT3026:Strictness: Const has same name as Const \'TestConst\'',
'26:LINT3026:Strictness: Const has same name as Namespace \'TestNamespace\''

]);
});

it('detects name shadowing function', async () => {
const diagnostics = await linter.run({
...project1,
files: ['source/name-shadowing-functions.bs'],
rules: {
'name-shadowing': 'error'
}
});
expectDiagnosticsFmt(diagnostics, [
'02:LINT3026:Strictness: Const has same name as Function \'TestFunction\'',
'03:LINT3026:Strictness: Const has same name as Global Function \'Lcase\''
]);
});
});

describe('fix', () => {
// Filenames (without the extension) that we want to copy with a "-temp" suffix
const tmpFileNames = [
Expand Down
112 changes: 111 additions & 1 deletion src/plugins/codeStyle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ import {
isVoidType,
Statement,
SymbolTypeFlag,
XmlFile
XmlFile,
AstNode,
Token,
isNamespaceStatement,
util,
isAnyReferenceType,
ExtraSymbolData,
OnScopeValidateEvent,
InternalWalkMode,
isCallableType
} from 'brighterscript';
import { RuleAAComma } from '../..';
import { addFixesToEvent } from '../../textEdit';
Expand All @@ -27,6 +36,7 @@ import { createColorValidator } from '../../createColorValidator';
import { messages } from './diagnosticMessages';
import { extractFixes } from './styleFixes';
import { BsLintDiagnosticContext } from '../../Linter';
import { Location } from 'vscode-languageserver-types';

export default class CodeStyle implements CompilerPlugin {

Expand Down Expand Up @@ -218,6 +228,33 @@ export default class CodeStyle implements CompilerPlugin {
return diagnostics;
}

validateBrsFileInScope(file: BrsFile) {
const diagnostics: (Omit<BsDiagnostic, 'file'>)[] = [];
const { severity } = this.lintContext;
const { nameShadowing } = severity;

file.ast.walk(createVisitor({
NamespaceStatement: (nsStmt) => {
this.validateNameShadowing(file, nsStmt, nsStmt.getNameParts()?.[0], nameShadowing, diagnostics);
},
ClassStatement: (classStmt) => {
this.validateNameShadowing(file, classStmt, classStmt.tokens.name, nameShadowing, diagnostics);
},
InterfaceStatement: (ifaceStmt) => {
this.validateNameShadowing(file, ifaceStmt, ifaceStmt.tokens.name, nameShadowing, diagnostics);
},
ConstStatement: (constStmt) => {
this.validateNameShadowing(file, constStmt, constStmt.tokens.name, nameShadowing, diagnostics);
},
EnumStatement: (enumStmt) => {
this.validateNameShadowing(file, enumStmt, enumStmt.tokens.name, nameShadowing, diagnostics);
}
// eslint-disable-next-line no-bitwise
}), { walkMode: WalkMode.visitStatementsRecursive | InternalWalkMode.visitFalseConditionalCompilationBlocks });

return diagnostics;
}

afterFileValidate(event: AfterFileValidateEvent) {
const { file } = event;
if (this.lintContext.ignores(file)) {
Expand Down Expand Up @@ -248,6 +285,35 @@ export default class CodeStyle implements CompilerPlugin {
event.program.diagnostics.register(bsDiagnostics, BsLintDiagnosticContext);
}

onScopeValidate(event: OnScopeValidateEvent) {
for (const file of event.scope.getOwnFiles()) {
if (this.lintContext.ignores(file)) {
return;
}

const diagnostics: (Omit<BsDiagnostic, 'file'>)[] = [];
if (isBrsFile(file)) {
diagnostics.push(...this.validateBrsFileInScope(file));
}

// add file reference
let bsDiagnostics: BsDiagnostic[] = diagnostics.map(diagnostic => ({
...diagnostic,
file
}));

const { fix } = this.lintContext;

// apply fix
if (fix) {
bsDiagnostics = extractFixes(this.lintContext.addFixes, bsDiagnostics);
}

// append diagnostics
event.program.diagnostics.register(bsDiagnostics, BsLintDiagnosticContext);
}
}

validateAAStyle(aa: AALiteralExpression, aaCommaStyle: RuleAAComma, diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
const indexes = collectWrappingAAMembersIndexes(aa);
const last = indexes.length - 1;
Expand Down Expand Up @@ -337,6 +403,50 @@ export default class CodeStyle implements CompilerPlugin {
}
return hasReturnedValue;
}

validateNameShadowing(file: BrsFile, node: AstNode, nameIdentifier: Token, severity: DiagnosticSeverity, diagnostics: (Omit<BsDiagnostic, 'file'>)[]) {
const name = nameIdentifier?.text;
if (!name || !node) {
return;
}
const nameLocation = nameIdentifier.location;

const astTable = file.ast.getSymbolTable();
const data = {} as ExtraSymbolData;
const typeChain = [];
// eslint-disable-next-line no-bitwise
const existingType = astTable.getSymbolType(name, { flags: SymbolTypeFlag.runtime | SymbolTypeFlag.typetime, data: data, typeChain: typeChain });

if (!existingType || isAnyReferenceType(existingType)) {
return;
}
if ((data.definingNode === node) || (isNamespaceStatement(data.definingNode) && isNamespaceStatement(node))) {
return;
}
const otherNode = data.definingNode as unknown as { tokens: { name: Token }; location: Location };
const thisNodeKindName = util.getAstNodeFriendlyName(node);
let thatNodeKindName = util.getAstNodeFriendlyName(data.definingNode) ?? '';
if (!thatNodeKindName && isCallableType(existingType)) {
thatNodeKindName = 'Global Function';
}

let thatNameLocation = otherNode?.tokens?.name?.location ?? otherNode?.location;

if (isNamespaceStatement(data.definingNode)) {
thatNameLocation = data.definingNode.getNameParts()?.[0]?.location;
}

const relatedInformation = thatNameLocation ? [{
message: `${thatNodeKindName} declared here`,
location: thatNameLocation
}] : undefined;

diagnostics.push({
...messages.nameShadowing(thisNodeKindName, thatNodeKindName, name, severity),
range: nameLocation.range,
relatedInformation: relatedInformation
});
}
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export function getDefaultRules(): BsLintConfig['rules'] {
'type-annotations': 'off',
'no-print': 'off',
'no-assocarray-component-field-type': 'off',
'no-array-component-field-type': 'off'
'no-array-component-field-type': 'off',
'name-shadowing': 'off'
};
}

Expand Down Expand Up @@ -166,7 +167,8 @@ function rulesToSeverity(rules: BsLintConfig['rules']) {
colorAlphaDefaults: rules['color-alpha-defaults'],
colorCertCompliant: rules['color-cert'],
noAssocarrayComponentFieldType: ruleToSeverity(rules['no-assocarray-component-field-type']),
noArrayComponentFieldType: ruleToSeverity(rules['no-array-component-field-type'])
noArrayComponentFieldType: ruleToSeverity(rules['no-array-component-field-type']),
nameShadowing: ruleToSeverity(rules['name-shadowing'])
};
}

Expand Down
8 changes: 8 additions & 0 deletions test/project1/source/name-shadowing-functions.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TestNamespace
const TestFunction = 3
const Lcase = 4
end namespace

function TestFunction()
return 2
end function
12 changes: 12 additions & 0 deletions test/project1/source/name-shadowing-import.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace TestNamespace
class TestImportClass
end class

enum TestImportEnum
end enum

interface TestImportInterface
end interface

const TestImportConst = 1
end namespace
40 changes: 40 additions & 0 deletions test/project1/source/name-shadowing.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class TestClass
end class

enum TestEnum
end enum

interface TestInterface
end interface

const TestConst = 1


namespace TestNamespace

class TestClass
end class

enum TestEnum
end enum

interface TestInterface
end interface

const TestConst = 1

const TestNamespace = 2

end namespace


class TestImportClass
end class

enum TestImportEnum
end enum

interface TestImportInterface
end interface

const TestImportConst = 1

0 comments on commit fa6a65c

Please sign in to comment.