Skip to content

Commit

Permalink
feat(missing members): implement members from extended interfaces
Browse files Browse the repository at this point in the history
re #6
  • Loading branch information
tamj0rd2 committed Nov 29, 2020
1 parent f4e0c7f commit d3c655a
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 89 deletions.
8 changes: 2 additions & 6 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"script": "build",
"problemMatcher": "$tsc",
"group": {
"kind": "build",
"isDefault": true
Expand Down
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,16 @@ It seems to get confused about where to find project files unless I do this.
1. Copy paste packages/example-package and give it a new name
2. Open this repo's root tsconfig file and add a project reference to your new package
3. Open this repo's root .eslintrc.js and add the package's tsconfig path to the `projects` array

## Monitoring the ts-server logs

In vscode, you can see some communications with the ts-server by looking at the Typescript output channel.

Here's a nice little script you can use to monitor the typescript server logs:

```bash
export TSS_LOG="-logToFile true -file <someFolderPath>/ts-logs.txt -level verbose"
tail -f ../ts-logs.txt | grep --line-buffered --color=always ts-quickfixes-plugin
```

If you're able to see `Hello world!` in the output, the monitoring is working
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
"pretest": "npm run build",
"test": "lerna run --stream test",
"test:plugin": "lerna run --stream --scope ts-quickfixes-plugin test",
"test:plugin:watch": "npx lerna exec --scope ts-quickfixes-plugin -- jest --watch",
"test:extension": "lerna run --stream --scope ts-quickfixes-extension test",
"test:e2e": "lerna run --stream --scope ts-quickfixes-e2e test",
"shipit": "lerna run --stream --concurrency 1 shipit",
"install-docs": "ts-node ./install-docs.ts"
"install-docs": "ts-node ./install-docs.ts",
"logs": "tail -f ../ts-logs.txt | grep --line-buffered --color=always ts-quickfixes-plugin"
},
"husky": {
"hooks": {
"pre-commit": "npm run install-docs && lint-staged",
"pre-commit": "run-s install-docs build && lint-staged",
"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true"
}
},
Expand All @@ -34,6 +36,7 @@
"@semantic-release/github": "^7.1.1",
"@types/fs-extra": "^9.0.4",
"@types/glob": "^7.1.3",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.6",
"@types/vscode": "^1.50.0",
"@typescript-eslint/eslint-plugin": "^4.1.1",
Expand All @@ -51,6 +54,7 @@
"jest": "^26.6.1",
"lerna": "^3.22.1",
"lint-staged": "^10.5.0",
"mock-fs": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"semantic-release": "^17.2.2",
Expand Down
15 changes: 15 additions & 0 deletions packages/e2e/src/fixtures/employee.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
department: 'todo',
firstName: 'todo',
lastName: 'todo',
birthday: null,
address: {
city: 'todo',
postcode: 'todo',
},
mobileNumber: {
countryCode: 'todo',
phoneNumber: 0,
},
status: null,
}
3 changes: 2 additions & 1 deletion packages/e2e/src/tests/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ describe('Acceptance tests', () => {
void vscode.window.showInformationMessage('Starting acceptance tests')
})

describe('Implement missing memebers', () => {
describe('Implement missing members', () => {
const happyPathCases = [
['implements all object members when all of them were missing', 'aPerson'],
['only implements missing members if some members are already defined', 'personWithOneProperty'],
['implements missing members for objects that have been defined on a single line', 'singleLinePerson'],
['implements missing members for interfaces that have been extended', 'employee'],
]

it.each(happyPathCases)('%s', async (_, variableName) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin/src/formatter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('Formatter', () => {
` phoneNumber: 0,`,
` },`,
` status: null,`,
` isEmployed: false,`,
`}`,
].join(lineEnding),
)
Expand Down Expand Up @@ -59,6 +60,7 @@ describe('Formatter', () => {
` phoneNumber: 0,`,
` },`,
` status: null,`,
` isEmployed: false,`,
`}`,
].join(lineEnding),
)
Expand All @@ -82,6 +84,7 @@ function createTestDeps() {
phoneNumber: MemberType.Number,
},
status: MemberType.Union,
isEmployed: MemberType.Boolean,
}),
}
}
29 changes: 25 additions & 4 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CodeFixProvider } from './code-fixes'
import { CodeFixProvider } from './providers'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function init(modules: Modules): { create: CreateFn } {
Expand All @@ -7,18 +7,39 @@ function init(modules: Modules): { create: CreateFn } {
const logger = {
info: (message: string | Record<string, unknown>): void =>
info.project.projectService.logger.info(
`ts-quickfixes-plugin: INFO: ${typeof message === 'object' ? JSON.stringify(message) : message}`,
`ts-quickfixes-plugin: INFO: ${
typeof message === 'object'
? JSON.stringify(message, (_, value) => (value === undefined ? null : value))
: message
}`,
),
error: (message: string | Error): void =>
info.project.projectService.logger.info(
`ts-quickfixes-plugin: ERROR: ${message instanceof Error ? message.stack : message}`,
`ts-quickfixes-plugin: ERROR: ${
message instanceof Error ? message.stack?.replace('\n', '. ') : message
}`,
),
}
logger.info('Hello world!')

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const logOnError = <F extends (...args: any[]) => any>(func: F) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (...args: any[]): ReturnType<F> => {
try {
return func(...args)
} catch (err) {
logger.error(err)
throw err
}
}
}

return {
...originalLanguageService,
getCodeFixesAtPosition: new CodeFixProvider(originalLanguageService, logger).getCodeFixesAtPosition,
getCodeFixesAtPosition: logOnError(
new CodeFixProvider(originalLanguageService, logger).getCodeFixesAtPosition,
),
}
}

Expand Down
82 changes: 62 additions & 20 deletions packages/plugin/src/member-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { resolve } from 'path'
import ts from 'typescript/lib/tsserverlibrary'
import { MemberParser, MemberType, VariableInfo } from './member-parser'
import mockFs from 'mock-fs'

describe('MemberParser', () => {
afterEach(() => mockFs.restore())

describe('getMissingMembersForVariable', () => {
it('returns the correct members when there are none specified', () => {
const { program, testFilePath } = createTestDeps()
const memberParser = new MemberParser(program)
const { createProgram, getFilePath } = createTestDeps()
const testFilePath = getFilePath('testing')
const memberParser = new MemberParser(createProgram(testFilePath))

const members = memberParser.getMissingMembersForVariable('aPerson', testFilePath)

Expand All @@ -21,8 +25,9 @@ describe('MemberParser', () => {
})

it('returns the correct members when there is already one specified', () => {
const { program, testFilePath } = createTestDeps()
const memberParser = new MemberParser(program)
const { createProgram, getFilePath } = createTestDeps()
const testFilePath = getFilePath('testing')
const memberParser = new MemberParser(createProgram(testFilePath))

const members = memberParser.getMissingMembersForVariable('personWithOneProperty', testFilePath)

Expand All @@ -34,12 +39,44 @@ describe('MemberParser', () => {
status: MemberType.Union,
})
})

describe('when the interface extends another interface', () => {
it('gets the correct members when they are declared in the same file', () => {
const { createProgram } = createTestDeps()
const filePath = 'file.ts'
mockFs({
[filePath]: `
interface Animal {
age: number
hasLegs: boolean
}
interface Dog extends Animal {
breed: string
}
export const dog: Dog = {}`,
})

const memberParser = new MemberParser(createProgram(filePath))
const members = memberParser.getMissingMembersForVariable('dog', filePath)

expect(members).toStrictEqual<typeof members>({
age: MemberType.Number,
breed: MemberType.String,
hasLegs: MemberType.Boolean,
})
})

it.todo('gets the correct members when they are declared in different files')
})
})

describe('getVariableInfo', () => {
it(`returns the variable's value when there are no members`, () => {
const { program, testFilePath } = createTestDeps()
const memberParser = new MemberParser(program)
const { createProgram, getFilePath } = createTestDeps()
const testFilePath = getFilePath('testing')
const memberParser = new MemberParser(createProgram(testFilePath))

const info = memberParser.getVariableInfo('aPerson', testFilePath)

Expand All @@ -51,8 +88,9 @@ describe('MemberParser', () => {
})

it(`returns the variable's value when there are some members`, () => {
const { program, testFilePath } = createTestDeps()
const memberParser = new MemberParser(program)
const { createProgram, getFilePath } = createTestDeps()
const testFilePath = getFilePath('testing')
const memberParser = new MemberParser(createProgram(testFilePath))

const info = memberParser.getVariableInfo('personWithOneProperty', testFilePath)

Expand All @@ -64,8 +102,9 @@ describe('MemberParser', () => {
})

it(`returns the variable's text when it is a single line declaration`, () => {
const { program, testFilePath } = createTestDeps()
const memberParser = new MemberParser(program)
const { createProgram, getFilePath } = createTestDeps()
const testFilePath = getFilePath('testing')
const memberParser = new MemberParser(createProgram(testFilePath))

const info = memberParser.getVariableInfo('singleLinePerson', testFilePath)

Expand All @@ -79,7 +118,10 @@ describe('MemberParser', () => {

describe('getVariableNameAtLocation', () => {
it('can get a variable name at a certain location', () => {
const { program, testFilePath } = createTestDeps()
const { createProgram, getFilePath } = createTestDeps()
const testFilePath = getFilePath('testing')
const program = createProgram(testFilePath)

const expectedVariableName = 'singleLinePerson'
const start = 376
const end = start + expectedVariableName.length
Expand All @@ -92,15 +134,15 @@ describe('MemberParser', () => {
})

function createTestDeps() {
const testFilePath = resolve(process.cwd(), `../../test-environment/testing.ts`)
const program = ts.createProgram([testFilePath], {
noEmit: true,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.Latest,
})

const fixtureFolder = resolve(process.cwd(), `../../test-environment`)
return {
testFilePath,
program,
createProgram: (...fileNames: [string, ...string[]]) =>
ts.createProgram(fileNames, {
noEmit: true,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.Latest,
}),
getFilePath: (fileName: string) => `${fixtureFolder}/${fileName}.ts`,
makeFileContent: (...lines: string[]) => lines.join('\n'),
}
}
Loading

0 comments on commit d3c655a

Please sign in to comment.