Repository: styled-components/stylelint-processor-styled-components (js)

Issue:
Not reporting errors when adding attaching additional props
I would expect this to fail:

```js
const StyledLightBulbIcon = styled(Icon).attrs({ iconName: 'light_bulb' })`
  svg-bad-selector {
    height: 50px;
  }
`
```

However it doesn't.  I passes without an issue.  When I remove the call to the `.attrs` function, it fails with an error.

```js
const StyledLightBulbIcon = styled(Icon)`
  svg-bad-selector {
    height: 50px;
  }
`
```

```
src/components/task/list.jsx
 10:3  ✖  Unexpected unknown type selector "svg-bad-selector"   selector-type-no-unknown
```

Is there an option that can be passed into the CLI to ensure that it validates the styles when the `.attrs` function is being used?

Pull request description:
Support Styled Component properties
I have currently implemented a fix for #106 (`.attrs`) but after double checking the Styled Components API I also want to put in a similar check for `.extend` before merging it in.

EDIT:
Done, this should add support for both `.extend` and `.attrs`, and is ready for review now.

resolves #106 

Patch:
diff --git a/src/parsers/index.js b/src/parsers/index.js
index 19a750b..36d847c 100644
--- a/src/parsers/index.js
+++ b/src/parsers/index.js
@@ -4,6 +4,9 @@ const traverse = require('babel-traverse').default
 const isStyled = require('../utils/styled').isStyled
 const isHelper = require('../utils/styled').isHelper
 const isStyledImport = require('../utils/styled').isStyledImport
+const hasAttrsCall = require('../utils/styled').hasAttrsCall
+const getAttrsObject = require('../utils/styled').getAttrsObject
+const isExtendCall = require('../utils/styled').isExtendCall
 
 const wrapSelector = require('../utils/general').wrapSelector
 const wrapKeyframes = require('../utils/general').wrapKeyframes
@@ -39,8 +42,12 @@ const processStyledComponentsFile = ast => {
         return
       }
       const helper = isHelper(node, importedNames)
-      if (!helper && !isStyled(node, importedNames.default)) return
-      const content = getTTLContent(node)
+      const processedNode = Object.assign({}, node)
+      if (hasAttrsCall(node)) {
+        processedNode.tag = getAttrsObject(node)
+      }
+      if (!helper && !isStyled(processedNode, importedNames.default) && !isExtendCall(node)) return
+      const content = getTTLContent(processedNode)
       const fixedContent = fixIndentation(content).text
       const wrapperFn = helper === 'keyframes' ? wrapKeyframes : wrapSelector
       const wrappedContent = wrapperFn(fixedContent)
@@ -50,7 +57,7 @@ const processStyledComponentsFile = ast => {
       extractedCSS.push(stylelintCommentsAdded)
       sourceMap = Object.assign(
         sourceMap,
-        getSourceMap(extractedCSS.join('\n'), wrappedContent, node.loc.start.line)
+        getSourceMap(extractedCSS.join('\n'), wrappedContent, processedNode.loc.start.line)
       )
       /**
        * All queued comments have been added to the file so we don't need to, and actually shouldn't
diff --git a/src/utils/styled.js b/src/utils/styled.js
index e253f5b..b33b58f 100644
--- a/src/utils/styled.js
+++ b/src/utils/styled.js
@@ -33,6 +33,20 @@ const isStyledCall = (node, styledVariableName) =>
   // And that the function name matches the imported name
   node.tag.callee.name === styledVariableName
 
+/**
+ * Check if it has a .attrs postfix which we in that case handle specially
+ */
+const hasAttrsCall = node =>
+  // Check that it's a function call
+  node.tag &&
+  node.tag.callee &&
+  // Check that the last member of the call is attrs
+  node.tag.callee.property &&
+  node.tag.callee.property.name === 'attrs'
+
+// We don't need the checks here as they were checked in hasAttrsCall
+const getAttrsObject = node => node.tag.callee.object
+
 /**
  * Check if something is a styled component call
  */
@@ -40,6 +54,13 @@ const isStyled = (node, styledVariableName) =>
   isTaggedTemplateLiteral(node) &&
   (isStyledCall(node, styledVariableName) || isStyledShorthand(node, styledVariableName))
 
+/**
+ * Check if it is a .extend call and we pretty reasonable assume that any TTL that ends
+ * in a .extend must be a styled components call as there is no way to check if it was
+ * called on a Styled Component
+ */
+const isExtendCall = node => node.tag && node.tag.property && node.tag.property.name === 'extend'
+
 /**
  * Check if something is a call to one of our helper methods
  *
@@ -63,3 +84,6 @@ exports.isStyledShorthand = isStyledShorthand
 exports.isStyledCall = isStyledCall
 exports.isStyled = isStyled
 exports.isHelper = isHelper
+exports.hasAttrsCall = hasAttrsCall
+exports.getAttrsObject = getAttrsObject
+exports.isExtendCall = isExtendCall


Test patch:
diff --git a/test/fixtures/simple/identify-styled.js b/test/fixtures/simple/identify-styled.js
new file mode 100644
index 0000000..6f12f4e
--- /dev/null
+++ b/test/fixtures/simple/identify-styled.js
@@ -0,0 +1,31 @@
+import styled from 'styled-components';
+
+const Button1 = styled.div`
+  bad-selector {
+    color: red;
+  }
+`;
+
+const Button2 = styled(Button1)`
+  bad-selector {
+    color: red;
+  }
+`;
+
+const Image1 = styled.img.attrs({ src: 'url' })`
+  bad-selector {
+    color: red;
+  }
+`;
+
+const Image2 = styled(Image1).attrs({ src: 'newUrl' })`
+  bad-selector {
+    color: red;
+  }
+`;
+
+const Image3 = Image2.extend`
+  bad-selector2 {
+    color: blue;
+  }
+`;
diff --git a/test/simple.test.js b/test/simple.test.js
index 85b047f..b7862b1 100644
--- a/test/simple.test.js
+++ b/test/simple.test.js
@@ -11,7 +11,8 @@ const rules = {
       except: ['first-nested'],
       ignore: ['after-comment']
     }
-  ]
+  ],
+  'selector-type-no-unknown': true
 }
 
 describe('simple', () => {
@@ -195,4 +196,44 @@ describe('simple', () => {
       expect(data.results[0].warnings.length).toEqual(8)
     })
   })
+
+  describe('identify styled', () => {
+    beforeAll(() => {
+      fixture = path.join(__dirname, './fixtures/simple/identify-styled.js')
+    })
+
+    it('should have one result', () => {
+      expect(data.results.length).toEqual(1)
+    })
+
+    it('should use the right file', () => {
+      expect(data.results[0].source).toEqual(fixture)
+    })
+
+    it('should have errored', () => {
+      expect(data.errored).toEqual(true)
+    })
+
+    it('should have 4 warnings', () => {
+      expect(data.results[0].warnings.length).toEqual(5)
+    })
+
+    it('should have correct warnings', () => {
+      const warnings = data.results[0].warnings
+      expect(warnings[0].line).toBe(4)
+      expect(warnings[0].rule).toBe('selector-type-no-unknown')
+
+      expect(warnings[1].line).toBe(10)
+      expect(warnings[1].rule).toBe('selector-type-no-unknown')
+
+      expect(warnings[2].line).toBe(16)
+      expect(warnings[2].rule).toBe('selector-type-no-unknown')
+
+      expect(warnings[3].line).toBe(22)
+      expect(warnings[3].rule).toBe('selector-type-no-unknown')
+
+      expect(warnings[4].line).toBe(28)
+      expect(warnings[4].rule).toBe('selector-type-no-unknown')
+    })
+  })
 })